From bc2095c842270b6b185f96a022cc2fa6c39bd094 Mon Sep 17 00:00:00 2001 From: Jacqueline Bronger Date: Tue, 1 Nov 2022 11:05:29 +0100 Subject: [PATCH 1/3] Use RecyclerView for TV PiP menu buttons. - Improved a11y messages: Talkback adds context of buttons (PiP menu) when focus switches to the RecyclerView + adds item position (X of X) - Improved readability without switching between different ScrollViews - Nicer scroll behavior at start and end than with Space elements - Also fixes move menu arrows not updating when moving the PiP Bug: 258652853 Test: manual - with and without Talkback: open PiP menu, enter/move/exit move menu, expand/collapse PiP with and without orientation change, have test app add and remove custom actions while the menu is open Change-Id: Idbe4c2abce148d0fd5921371b1c95f8dafc3da5f --- .../Shell/res/layout/tv_pip_menu.xml | 70 +-- .../layout/tv_window_menu_action_button.xml | 1 + .../Shell/res/values-tvdpi/dimen.xml | 2 +- .../common/TvWindowMenuActionButton.java | 55 +- .../android/wm/shell/pip/tv/TvPipAction.java | 84 +++ .../wm/shell/pip/tv/TvPipCustomAction.java | 59 ++ .../wm/shell/pip/tv/TvPipMenuController.java | 10 +- .../wm/shell/pip/tv/TvPipMenuView.java | 555 ++++++++---------- .../wm/shell/pip/tv/TvPipSystemAction.java | 58 ++ 9 files changed, 484 insertions(+), 410 deletions(-) create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml index 70755e6cc3cfc..dcce4698c2524 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -44,67 +44,15 @@ android:background="@color/tv_pip_menu_dim_layer" android:alpha="0"/> - - - - - - - - - - - - - - - - - - - - - - + diff --git a/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml index c4dbd39c729a6..b2ac85b018be9 100644 --- a/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml +++ b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml @@ -21,6 +21,7 @@ android:layout_width="@dimen/tv_window_menu_button_size" android:layout_height="@dimen/tv_window_menu_button_size" android:padding="@dimen/tv_window_menu_button_margin" + android:duplicateParentState="true" android:stateListAnimator="@animator/tv_window_menu_action_button_animator" android:focusable="true"> diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml index 9833a88a1c0a2..0b61d7a85d9ee 100644 --- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml +++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml @@ -28,7 +28,7 @@ 6dp 4dp 24dp - 26dp + 30dp 20dp diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java index 39b0b5500ceaf..8ba785a1f03ab 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java @@ -19,6 +19,8 @@ package com.android.wm.shell.common; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Handler; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -30,11 +32,11 @@ import com.android.wm.shell.R; /** * A common action button for TV window menu layouts. */ -public class TvWindowMenuActionButton extends RelativeLayout implements View.OnClickListener { +public class TvWindowMenuActionButton extends RelativeLayout { private final ImageView mIconImageView; private final View mButtonBackgroundView; - private final View mButtonView; - private OnClickListener mOnClickListener; + + private Icon mCurrentIcon; public TvWindowMenuActionButton(Context context) { this(context, null, 0, 0); @@ -56,7 +58,6 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC inflater.inflate(R.layout.tv_window_menu_action_button, this); mIconImageView = findViewById(R.id.icon); - mButtonView = findViewById(R.id.button); mButtonBackgroundView = findViewById(R.id.background); final int[] values = new int[]{android.R.attr.src, android.R.attr.text}; @@ -71,23 +72,6 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC typedArray.recycle(); } - @Override - public void setOnClickListener(OnClickListener listener) { - // We do not want to set an OnClickListener to the TvWindowMenuActionButton itself, but only - // to the ImageView. So let's "cash" the listener we've been passed here and set a "proxy" - // listener to the ImageView. - mOnClickListener = listener; - mButtonView.setOnClickListener(listener != null ? this : null); - } - - @Override - public void onClick(View v) { - if (mOnClickListener != null) { - // Pass the correct view - this. - mOnClickListener.onClick(this); - } - } - /** * Sets the drawable for the button with the given drawable. */ @@ -104,11 +88,24 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC } } + public void setImageIconAsync(Icon icon, Handler handler) { + mCurrentIcon = icon; + // Remove old image while waiting for the new one to load. + mIconImageView.setImageDrawable(null); + icon.loadDrawableAsync(mContext, d -> { + // The image hasn't been set any other way and the drawable belongs to the most + // recently set Icon. + if (mIconImageView.getDrawable() == null && mCurrentIcon == icon) { + mIconImageView.setImageDrawable(d); + } + }, handler); + } + /** * Sets the text for description the with the given string. */ public void setTextAndDescription(CharSequence text) { - mButtonView.setContentDescription(text); + setContentDescription(text); } /** @@ -118,16 +115,6 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC setTextAndDescription(getContext().getString(resId)); } - @Override - public void setEnabled(boolean enabled) { - mButtonView.setEnabled(enabled); - } - - @Override - public boolean isEnabled() { - return mButtonView.isEnabled(); - } - /** * Marks this button as a custom close action button. * This changes the style of the action button to highlight that this action finishes the @@ -147,10 +134,10 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC @Override public String toString() { - if (mButtonView.getContentDescription() == null) { + if (getContentDescription() == null) { return TvWindowMenuActionButton.class.getSimpleName(); } - return mButtonView.getContentDescription().toString(); + return getContentDescription().toString(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java new file mode 100644 index 0000000000000..9f8f95c93fec2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 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.annotation.IntDef; +import android.annotation.NonNull; +import android.app.PendingIntent; +import android.os.Handler; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.TvWindowMenuActionButton; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +abstract class TvPipAction { + + private static final String TAG = TvPipAction.class.getSimpleName(); + + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"ACTION_"}, value = { + ACTION_FULLSCREEN, + ACTION_CLOSE, + ACTION_MOVE, + ACTION_EXPAND_COLLAPSE, + ACTION_CUSTOM, + ACTION_CUSTOM_CLOSE + }) + public @interface ActionType { + } + + public static final int ACTION_FULLSCREEN = 0; + public static final int ACTION_CLOSE = 1; + public static final int ACTION_MOVE = 2; + public static final int ACTION_EXPAND_COLLAPSE = 3; + public static final int ACTION_CUSTOM = 4; + public static final int ACTION_CUSTOM_CLOSE = 5; + + @ActionType + private final int mActionType; + + TvPipAction(@ActionType int actionType) { + mActionType = actionType; + } + + boolean isCloseAction() { + return mActionType == ACTION_CLOSE || mActionType == ACTION_CUSTOM_CLOSE; + } + + @ActionType + int getActionType() { + return mActionType; + } + + abstract void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler); + + abstract PendingIntent getPendingIntent(); + + void executePendingIntent() { + if (getPendingIntent() == null) return; + try { + getPendingIntent().send(); + } catch (PendingIntent.CanceledException e) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to send action, %s", TAG, e); + } + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java new file mode 100644 index 0000000000000..5c0b1b3aa383d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 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.annotation.NonNull; +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.os.Handler; + +import com.android.wm.shell.common.TvWindowMenuActionButton; + +import java.util.List; +import java.util.Objects; + +/** + * A TvPipAction for actions that the app provides via {@link + * android.app.PictureInPictureParams.Builder#setCloseAction(RemoteAction)} or {@link + * android.app.PictureInPictureParams.Builder#setActions(List)}. + */ +public class TvPipCustomAction extends TvPipAction { + + private final RemoteAction mRemoteAction; + + TvPipCustomAction(@ActionType int actionType, @NonNull RemoteAction remoteAction) { + super(actionType); + Objects.requireNonNull(remoteAction); + mRemoteAction = remoteAction; + } + + void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler) { + if (button == null || mainHandler == null) return; + if (mRemoteAction.getContentDescription().length() > 0) { + button.setTextAndDescription(mRemoteAction.getContentDescription()); + } else { + button.setTextAndDescription(mRemoteAction.getTitle()); + } + button.setImageIconAsync(mRemoteAction.getIcon(), mainHandler); + button.setEnabled(isCloseAction() || mRemoteAction.isEnabled()); + } + + PendingIntent getPendingIntent() { + return mRemoteAction.getActionIntent(); + } + +} 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 c39400ab61e3f..e80602ea0cd92 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 @@ -146,7 +146,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis int pipMenuBorderWidth = mContext.getResources() .getDimensionPixelSize(R.dimen.pip_menu_border_width); mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipMenuBorderWidth, - -pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth)); + -pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth)); mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -pipEduTextHeight)); } @@ -221,7 +221,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (mInMoveMode) { mPipMenuView.showMoveMenu(mDelegate.getPipGravity()); } else { - mPipMenuView.showButtonsMenu(); + mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ false); } mPipMenuView.updateBounds(mTvPipBoundsState.getBounds()); } @@ -294,7 +294,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } if (mInMoveMode) { setInMoveMode(false); - mPipMenuView.showButtonsMenu(); + mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ true); return true; } return false; @@ -360,9 +360,9 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis return; } if (!mAppActions.isEmpty()) { - mPipMenuView.setAdditionalActions(mAppActions, mCloseAction, mMainHandler); + mPipMenuView.setAdditionalActions(mAppActions, mCloseAction); } else { - mPipMenuView.setAdditionalActions(mMediaActions, mCloseAction, mMainHandler); + mPipMenuView.setAdditionalActions(mMediaActions, mCloseAction); } } 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 57e95c416b3c4..d8dc022ad59c2 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 @@ -25,28 +25,30 @@ import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT; import static android.view.KeyEvent.KEYCODE_DPAD_UP; import static android.view.KeyEvent.KEYCODE_ENTER; -import android.app.PendingIntent; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; + import android.app.RemoteAction; import android.content.Context; import android.graphics.Rect; import android.os.Handler; import android.view.Gravity; import android.view.KeyEvent; -import android.view.SurfaceControl; import android.view.View; import android.view.ViewGroup; -import android.view.ViewRootImpl; import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; -import android.widget.HorizontalScrollView; import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ScrollView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.widget.LinearLayoutManager; +import com.android.internal.widget.RecyclerView; import com.android.wm.shell.R; import com.android.wm.shell.common.TvWindowMenuActionButton; import com.android.wm.shell.pip.PipUtils; @@ -60,84 +62,92 @@ import java.util.List; * actions: Fullscreen, Move and Close, but could also display "additional" actions, that may be set * via a {@link #setAdditionalActions(List, RemoteAction, Handler)} call. */ -public class TvPipMenuView extends FrameLayout implements View.OnClickListener { +public class TvPipMenuView extends FrameLayout { private static final String TAG = "TvPipMenuView"; - private static final int FIRST_CUSTOM_ACTION_POSITION = 3; + private static final int CLOSE_ACTION_INDEX = 1; + private static final int FIRST_CUSTOM_ACTION_INDEX = 2; - private final Listener mListener; + private final TvPipMenuView.Listener mListener; + + private final List mActionsList; + private final TvPipSystemAction mDefaultCloseAction; + private final TvPipSystemAction mExpandCollapseAction; + + private final RecyclerView mActionButtonsRecyclerView; + private final LinearLayoutManager mButtonLayoutManager; + private final RecyclerViewAdapter mRecyclerViewAdapter; - private final LinearLayout mActionButtonsContainer; - private final View mMenuFrameView; - private final List mAdditionalButtons = new ArrayList<>(); private final View mPipFrameView; + private final View mMenuFrameView; private final View mPipView; + + private final View mPipBackground; + private final View mDimLayer; + private final TvPipMenuEduTextDrawer mEduTextDrawer; + private final int mPipMenuOuterSpace; private final int mPipMenuBorderWidth; + private final int mPipMenuFadeAnimationDuration; + private final int mResizeAnimationDuration; + private final ImageView mArrowUp; private final ImageView mArrowRight; private final ImageView mArrowDown; private final ImageView mArrowLeft; private final TvWindowMenuActionButton mA11yDoneButton; - private final View mPipBackground; - private final View mDimLayer; - - private final ScrollView mScrollView; - private final HorizontalScrollView mHorizontalScrollView; - private View mFocusedButton; - private Rect mCurrentPipBounds; private boolean mMoveMenuIsVisible; private boolean mButtonMenuIsVisible; - - private final TvWindowMenuActionButton mExpandButton; - private final TvWindowMenuActionButton mCloseButton; - private boolean mSwitchingOrientation; - private final int mPipMenuFadeAnimationDuration; - private final int mResizeAnimationDuration; - private final AccessibilityManager mA11yManager; private final Handler mMainHandler; public TvPipMenuView(@NonNull Context context, @NonNull Handler mainHandler, @NonNull Listener listener) { super(context, null, 0, 0); - inflate(context, R.layout.tv_pip_menu, this); mMainHandler = mainHandler; mListener = listener; - mA11yManager = context.getSystemService(AccessibilityManager.class); - mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons); - mActionButtonsContainer.findViewById(R.id.tv_pip_menu_fullscreen_button) - .setOnClickListener(this); + mActionButtonsRecyclerView = findViewById(R.id.tv_pip_menu_action_buttons); + mButtonLayoutManager = new LinearLayoutManager(mContext); + mActionButtonsRecyclerView.setLayoutManager(mButtonLayoutManager); + mActionButtonsRecyclerView.setPreserveFocusAfterLayout(true); - mCloseButton = mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button); - mCloseButton.setOnClickListener(this); - mCloseButton.setIsCustomCloseAction(true); + mDefaultCloseAction = + new TvPipSystemAction(ACTION_CLOSE, R.string.pip_close, + R.drawable.pip_ic_close_white); + mExpandCollapseAction = + new TvPipSystemAction(ACTION_EXPAND_COLLAPSE, R.string.pip_collapse, + R.drawable.pip_ic_collapse); - mActionButtonsContainer.findViewById(R.id.tv_pip_menu_move_button) - .setOnClickListener(this); - mExpandButton = findViewById(R.id.tv_pip_menu_expand_button); - mExpandButton.setOnClickListener(this); + mActionsList = new ArrayList<>(); + mActionsList.add( + new TvPipSystemAction(ACTION_FULLSCREEN, R.string.pip_fullscreen, + R.drawable.pip_ic_fullscreen_white)); + mActionsList.add(mDefaultCloseAction); + mActionsList.add( + new TvPipSystemAction(ACTION_MOVE, R.string.pip_move, + R.drawable.pip_ic_move_white)); + mActionsList.add(mExpandCollapseAction); - mPipBackground = findViewById(R.id.tv_pip_menu_background); - mDimLayer = findViewById(R.id.tv_pip_menu_dim_layer); - - mScrollView = findViewById(R.id.tv_pip_menu_scroll); - mHorizontalScrollView = findViewById(R.id.tv_pip_menu_horizontal_scroll); + mRecyclerViewAdapter = new RecyclerViewAdapter(mActionsList); + mActionButtonsRecyclerView.setAdapter(mRecyclerViewAdapter); mMenuFrameView = findViewById(R.id.tv_pip_menu_frame); mPipFrameView = findViewById(R.id.tv_pip_border); mPipView = findViewById(R.id.tv_pip); + mPipBackground = findViewById(R.id.tv_pip_menu_background); + mDimLayer = findViewById(R.id.tv_pip_menu_dim_layer); + 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); @@ -160,8 +170,12 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { } void onPipTransitionToTargetBoundsStarted(Rect targetBounds) { + if (targetBounds == null) { + return; + } + // Fade out content by fading in view on top. - if (mCurrentPipBounds != null && targetBounds != null) { + if (mCurrentPipBounds != null) { boolean ratioChanged = PipUtils.aspectRatioChanged( mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(), targetBounds.width() / (float) targetBounds.height()); @@ -177,7 +191,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { // Update buttons. final boolean vertical = targetBounds.height() > targetBounds.width(); final boolean orientationChanged = - vertical != (mActionButtonsContainer.getOrientation() == LinearLayout.VERTICAL); + vertical != (mButtonLayoutManager.getOrientation() == LinearLayoutManager.VERTICAL); ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransitionToTargetBoundsStarted(), orientation changed %b", TAG, orientationChanged); @@ -187,19 +201,19 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { if (mButtonMenuIsVisible) { mSwitchingOrientation = true; - mActionButtonsContainer.animate() + mActionButtonsRecyclerView.animate() .alpha(0) .setInterpolator(TvPipInterpolators.EXIT) .setDuration(mResizeAnimationDuration / 2) .withEndAction(() -> { - changeButtonScrollOrientation(targetBounds); - updateButtonGravity(targetBounds); + mButtonLayoutManager.setOrientation(vertical + ? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL); // Only make buttons visible again in onPipTransitionFinished to keep in // sync with PiP content alpha animation. }); } else { - changeButtonScrollOrientation(targetBounds); - updateButtonGravity(targetBounds); + mButtonLayoutManager.setOrientation(vertical + ? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL); } } @@ -207,7 +221,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransitionFinished()", TAG); - // Fade in content by fading out view on top. + // Fade in content by fading out view on top (faded out at every aspect ratio change). mPipBackground.animate() .alpha(0f) .setDuration(mResizeAnimationDuration / 2) @@ -218,16 +232,14 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mEduTextDrawer.init(); } + // Update buttons. setIsExpanded(isTvPipExpanded); - // Update buttons. if (mSwitchingOrientation) { - mActionButtonsContainer.animate() + mActionButtonsRecyclerView.animate() .alpha(1) .setInterpolator(TvPipInterpolators.ENTER) .setDuration(mResizeAnimationDuration / 2); - } else { - refocusPreviousButton(); } mSwitchingOrientation = false; } @@ -240,107 +252,9 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { "%s: updateLayout, width: %s, height: %s", TAG, updatedBounds.width(), updatedBounds.height()); mCurrentPipBounds = updatedBounds; - if (!mSwitchingOrientation) { - updateButtonGravity(mCurrentPipBounds); - } - updatePipFrameBounds(); } - private void changeButtonScrollOrientation(Rect bounds) { - final boolean vertical = bounds.height() > bounds.width(); - - final ViewGroup oldScrollView = vertical ? mHorizontalScrollView : mScrollView; - final ViewGroup newScrollView = vertical ? mScrollView : mHorizontalScrollView; - - if (oldScrollView.getChildCount() == 1) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: orientation changed", TAG); - oldScrollView.removeView(mActionButtonsContainer); - oldScrollView.setVisibility(GONE); - mActionButtonsContainer.setOrientation(vertical ? LinearLayout.VERTICAL - : LinearLayout.HORIZONTAL); - newScrollView.addView(mActionButtonsContainer); - newScrollView.setVisibility(VISIBLE); - if (mFocusedButton != null) { - mFocusedButton.requestFocus(); - } - } - } - - /** - * Change button gravity based on new dimensions - */ - private void updateButtonGravity(Rect bounds) { - final boolean vertical = bounds.height() > bounds.width(); - // Use Math.max since the possible orientation change might not have been applied yet. - final int buttonsSize = Math.max(mActionButtonsContainer.getHeight(), - mActionButtonsContainer.getWidth()); - - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: buttons container width: %s, height: %s", TAG, - mActionButtonsContainer.getWidth(), mActionButtonsContainer.getHeight()); - - final boolean buttonsFit = - vertical ? buttonsSize < bounds.height() - : buttonsSize < bounds.width(); - final int buttonGravity = buttonsFit ? Gravity.CENTER - : (vertical ? Gravity.CENTER_HORIZONTAL : Gravity.CENTER_VERTICAL); - - final LayoutParams params = (LayoutParams) mActionButtonsContainer.getLayoutParams(); - params.gravity = buttonGravity; - mActionButtonsContainer.setLayoutParams(params); - - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: vertical: %b, buttonsFit: %b, gravity: %s", TAG, vertical, buttonsFit, - Gravity.toString(buttonGravity)); - } - - private void refocusPreviousButton() { - if (mMoveMenuIsVisible || mCurrentPipBounds == null || mFocusedButton == null) { - return; - } - final boolean vertical = mCurrentPipBounds.height() > mCurrentPipBounds.width(); - - if (!mFocusedButton.hasFocus()) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: request focus from: %s", TAG, mFocusedButton); - mFocusedButton.requestFocus(); - } else { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: already focused: %s", TAG, mFocusedButton); - } - - // Do we need to scroll? - final Rect buttonBounds = new Rect(); - final Rect scrollBounds = new Rect(); - if (vertical) { - mScrollView.getDrawingRect(scrollBounds); - } else { - mHorizontalScrollView.getDrawingRect(scrollBounds); - } - mFocusedButton.getHitRect(buttonBounds); - - if (scrollBounds.contains(buttonBounds)) { - // Button is already completely visible, don't scroll - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: not scrolling", TAG); - return; - } - - // Scrolling so the button is visible to the user. - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: scrolling to focused button", TAG); - - if (vertical) { - mScrollView.smoothScrollTo((int) mFocusedButton.getX(), - (int) mFocusedButton.getY()); - } else { - mHorizontalScrollView.smoothScrollTo((int) mFocusedButton.getX(), - (int) mFocusedButton.getY()); - } - } - Rect getPipMenuContainerBounds(Rect pipBounds) { final Rect menuUiBounds = new Rect(pipBounds); menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace); @@ -370,20 +284,34 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mPipView.setLayoutParams(pipViewParams); } - + // Keep focused button within the visible area while the PiP is changing size. Otherwise, + // the button would lose focus which would cause a need for scrolling and re-focusing after + // the animation finishes, which does not look good. + View focusedChild = mActionButtonsRecyclerView.getFocusedChild(); + if (focusedChild != null) { + mActionButtonsRecyclerView.scrollToPosition( + mActionButtonsRecyclerView.getChildLayoutPosition(focusedChild)); + } } void setExpandedModeEnabled(boolean enabled) { - mExpandButton.setVisibility(enabled ? VISIBLE : GONE); + int actionIndex = mActionsList.indexOf(mExpandCollapseAction); + boolean actionInList = actionIndex != -1; + if (enabled && !actionInList) { + mActionsList.add(mExpandCollapseAction); + mRecyclerViewAdapter.notifyItemInserted(mActionsList.size() - 1); + } else if (!enabled && actionInList) { + mActionsList.remove(actionIndex); + mRecyclerViewAdapter.notifyItemRemoved(actionIndex); + } } void setIsExpanded(boolean expanded) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: setIsExpanded, expanded: %b", TAG, expanded); - mExpandButton.setImageResource( + mExpandCollapseAction.update(expanded ? R.string.pip_collapse : R.string.pip_expand, expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand); - mExpandButton.setTextAndDescription( - expanded ? R.string.pip_collapse : R.string.pip_expand); + mRecyclerViewAdapter.notifyItemChanged(mActionsList.indexOf(mExpandCollapseAction)); } /** @@ -391,48 +319,75 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { */ void showMoveMenu(int gravity) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG); - showButtonsMenu(false); showMovementHints(gravity); + setMenuButtonsVisible(false); setFrameHighlighted(true); - mHorizontalScrollView.setFocusable(false); - mScrollView.setFocusable(false); + animateAlphaTo(mA11yManager.isEnabled() ? 1f : 0f, mDimLayer); mEduTextDrawer.closeIfNeeded(); } - void showButtonsMenu() { + + void showButtonsMenu(boolean exitingMoveMode) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showButtonsMenu()", TAG); - showButtonsMenu(true); + "%s: showButtonsMenu(), exitingMoveMode %b", TAG, exitingMoveMode); + setMenuButtonsVisible(true); hideMovementHints(); setFrameHighlighted(true); + animateAlphaTo(1f, mDimLayer); + mEduTextDrawer.closeIfNeeded(); - mHorizontalScrollView.setFocusable(true); - mScrollView.setFocusable(true); - - // Always focus on the first button when opening the menu, except directly after moving. - if (mFocusedButton == null) { - // Focus on first button (there is a Space at position 0) - mFocusedButton = mActionButtonsContainer.getChildAt(1); - // Reset scroll position. - mScrollView.scrollTo(0, 0); - mHorizontalScrollView.scrollTo( - isLayoutRtl() ? mActionButtonsContainer.getWidth() : 0, 0); + if (exitingMoveMode) { + scrollAndRefocusButton(getFirstIndexOfAction(ACTION_MOVE), + /* alwaysScroll= */ false); + } else { + scrollAndRefocusButton(0, /* alwaysScroll= */ true); + } + } + + private void scrollAndRefocusButton(int position, boolean alwaysScroll) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: scrollAndRefocusButton, target: %d", TAG, position); + + if (alwaysScroll || !refocusButton(position)) { + mButtonLayoutManager.scrollToPositionWithOffset(position, 0); + mActionButtonsRecyclerView.post(() -> refocusButton(position)); } - refocusPreviousButton(); } /** - * Hides all menu views, including the menu frame. + * @return true if focus was requested, false if focus request could not be carried out due to + * the view for the position not being available (scrolling beforehand will be necessary). */ + private boolean refocusButton(int position) { + View itemToFocus = mButtonLayoutManager.findViewByPosition(position); + if (itemToFocus != null) { + itemToFocus.requestFocus(); + itemToFocus.requestAccessibilityFocus(); + } + return itemToFocus != null; + } + + /** + * Returns the position of the first action of the given action type or -1 if none can be found. + */ + private int getFirstIndexOfAction(@TvPipAction.ActionType int actionType) { + for (int i = 0; i < mActionsList.size(); i++) { + if (mActionsList.get(i).getActionType() == actionType) { + return i; + } + } + return -1; + } + void hideAllUserControls() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: hideAllUserControls()", TAG); - mFocusedButton = null; - showButtonsMenu(false); + setMenuButtonsVisible(false); hideMovementHints(); setFrameHighlighted(false); + animateAlphaTo(0f, mDimLayer); } @Override @@ -463,134 +418,63 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { }); } - /** - * Button order: - * - Fullscreen - * - Close - * - Custom actions (app or media actions) - * - System actions - */ - void setAdditionalActions(List actions, RemoteAction closeAction, - Handler mainHandler) { + void setAdditionalActions(List actions, RemoteAction closeAction) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setAdditionalActions()", TAG); + "%s: setAdditionalActions(), %d actions", TAG, actions.size()); - // Replace system close action with custom close action if available - if (closeAction != null) { - setActionForButton(closeAction, mCloseButton, mainHandler); - } else { - mCloseButton.setTextAndDescription(R.string.pip_close); - mCloseButton.setImageResource(R.drawable.pip_ic_close_white); - } - mCloseButton.setIsCustomCloseAction(closeAction != null); - // Make sure the close action is always enabled - mCloseButton.setEnabled(true); - - // Make sure we exactly as many additional buttons as we have actions to display. - final int actionsNumber = actions.size(); - int buttonsNumber = mAdditionalButtons.size(); - if (actionsNumber > buttonsNumber) { - // Add buttons until we have enough to display all the actions. - while (actionsNumber > buttonsNumber) { - TvWindowMenuActionButton button = new TvWindowMenuActionButton(mContext); - button.setOnClickListener(this); - - mActionButtonsContainer.addView(button, - FIRST_CUSTOM_ACTION_POSITION + buttonsNumber); - mAdditionalButtons.add(button); - - buttonsNumber++; - } - } else if (actionsNumber < buttonsNumber) { - // Hide buttons until we as many as the actions. - while (actionsNumber < buttonsNumber) { - final View button = mAdditionalButtons.get(buttonsNumber - 1); - button.setVisibility(View.GONE); - button.setTag(null); - - buttonsNumber--; + int oldCustomActionCount = 0; + for (TvPipAction action : mActionsList) { + if (action.getActionType() == ACTION_CUSTOM) { + oldCustomActionCount++; } } - // "Assign" actions to the buttons. - for (int index = 0; index < actionsNumber; index++) { - final RemoteAction action = actions.get(index); - final TvWindowMenuActionButton button = mAdditionalButtons.get(index); + // Update close action. + mActionsList.set(CLOSE_ACTION_INDEX, + closeAction == null ? mDefaultCloseAction + : new TvPipCustomAction(ACTION_CUSTOM_CLOSE, closeAction)); + mRecyclerViewAdapter.notifyItemChanged(CLOSE_ACTION_INDEX); - // Remove action if it matches the custom close action. - if (PipUtils.remoteActionsMatch(action, closeAction)) { - button.setVisibility(GONE); + // Replace custom actions with new ones. + mActionsList.removeIf(tvPipAction -> tvPipAction.getActionType() == ACTION_CUSTOM); + List customActions = new ArrayList<>(actions.size()); + int newCustomActionCount = 0; + for (RemoteAction action : actions) { + if (action == null || PipUtils.remoteActionsMatch(action, closeAction)) { + // Don't show an action if it is the same as the custom close action continue; } - setActionForButton(action, button, mainHandler); + customActions.add(new TvPipCustomAction(ACTION_CUSTOM, action)); + newCustomActionCount++; } + mActionsList.addAll(FIRST_CUSTOM_ACTION_INDEX, customActions); - if (mCurrentPipBounds != null) { - updateButtonGravity(mCurrentPipBounds); - refocusPreviousButton(); - } - } + mRecyclerViewAdapter.notifyItemRangeChanged( + FIRST_CUSTOM_ACTION_INDEX, Math.min(oldCustomActionCount, newCustomActionCount)); - private void setActionForButton(RemoteAction action, TvWindowMenuActionButton button, - Handler mainHandler) { - button.setVisibility(View.VISIBLE); // Ensure the button is visible. - if (action.getContentDescription().length() > 0) { - button.setTextAndDescription(action.getContentDescription()); - } else { - button.setTextAndDescription(action.getTitle()); - } - button.setEnabled(action.isEnabled()); - button.setTag(action); - action.getIcon().loadDrawableAsync(mContext, button::setImageDrawable, mainHandler); - } + if (newCustomActionCount > oldCustomActionCount) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: setAdditionalActions(), %d inserted starting at %d", + TAG, newCustomActionCount - oldCustomActionCount, + FIRST_CUSTOM_ACTION_INDEX + oldCustomActionCount); + mRecyclerViewAdapter.notifyItemRangeInserted( + FIRST_CUSTOM_ACTION_INDEX + oldCustomActionCount, + newCustomActionCount - oldCustomActionCount); - @Nullable - SurfaceControl getWindowSurfaceControl() { - final ViewRootImpl root = getViewRootImpl(); - if (root == null) { - return null; - } - final SurfaceControl out = root.getSurfaceControl(); - if (out != null && out.isValid()) { - return out; - } - return null; - } - - @Override - public void onClick(View v) { - 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 if (id == R.id.tv_pip_menu_expand_button) { - mListener.onToggleExpandedMode(); - } else { - // This should be an "additional action" - final RemoteAction action = (RemoteAction) v.getTag(); - if (action != null) { - try { - action.getActionIntent().send(); - } catch (PendingIntent.CanceledException e) { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Failed to send action, %s", TAG, e); - } - } else { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: RemoteAction is null", TAG); - } + } else if (oldCustomActionCount > newCustomActionCount) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: setAdditionalActions(), %d removed starting at %d", + TAG, oldCustomActionCount - newCustomActionCount, + FIRST_CUSTOM_ACTION_INDEX + newCustomActionCount); + mRecyclerViewAdapter.notifyItemRangeRemoved( + FIRST_CUSTOM_ACTION_INDEX + newCustomActionCount, + oldCustomActionCount - newCustomActionCount); } } @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == ACTION_UP) { - if (!mMoveMenuIsVisible) { - mFocusedButton = mActionButtonsContainer.getFocusedChild(); - } if (event.getKeyCode() == KEYCODE_BACK) { mListener.onBackPress(); @@ -624,10 +508,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { public void showMovementHints(int gravity) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity)); - - if (mMoveMenuIsVisible) { - return; - } mMoveMenuIsVisible = true; animateAlphaTo(checkGravity(gravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp); @@ -643,9 +523,12 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { animateAlphaTo(a11yEnabled ? 1f : 0f, mA11yDoneButton); if (a11yEnabled) { + mA11yDoneButton.setVisibility(VISIBLE); mA11yDoneButton.setOnClickListener(v -> { mListener.onExitMoveMode(); }); + mA11yDoneButton.requestFocus(); + mA11yDoneButton.requestAccessibilityFocus(); } } @@ -684,27 +567,81 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { /** * Show or hide the pip buttons menu. */ - public void showButtonsMenu(boolean show) { + private void setMenuButtonsVisible(boolean visible) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showUserActions: %b", TAG, show); - if (mButtonMenuIsVisible == show) { - return; - } - mButtonMenuIsVisible = show; - - if (show) { - mActionButtonsContainer.setVisibility(VISIBLE); - refocusPreviousButton(); - } - animateAlphaTo(show ? 1 : 0, mActionButtonsContainer); - animateAlphaTo(show ? 1 : 0, mDimLayer); - mEduTextDrawer.closeIfNeeded(); + "%s: showUserActions: %b", TAG, visible); + mButtonMenuIsVisible = visible; + animateAlphaTo(visible ? 1 : 0, mActionButtonsRecyclerView); } private void setFrameHighlighted(boolean highlighted) { mMenuFrameView.setActivated(highlighted); } + private class RecyclerViewAdapter extends + RecyclerView.Adapter { + + private final List mActionList; + + RecyclerViewAdapter(List actionList) { + this.mActionList = actionList; + } + + @NonNull + @Override + public ButtonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ButtonViewHolder(new TvWindowMenuActionButton(mContext)); + } + + @Override + public void onBindViewHolder(@NonNull ButtonViewHolder holder, int position) { + TvPipAction action = mActionList.get(position); + action.populateButton(holder.mButton, mMainHandler); + } + + @Override + public int getItemCount() { + return mActionList.size(); + } + + private class ButtonViewHolder extends RecyclerView.ViewHolder implements OnClickListener { + TvWindowMenuActionButton mButton; + + ButtonViewHolder(@NonNull View itemView) { + super(itemView); + mButton = (TvWindowMenuActionButton) itemView; + mButton.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + TvPipAction action = mActionList.get( + mActionButtonsRecyclerView.getChildLayoutPosition(v)); + switch (action.getActionType()) { + case ACTION_FULLSCREEN: + mListener.onFullscreenButtonClick(); + return; + case ACTION_CLOSE: + case ACTION_CUSTOM_CLOSE: + mListener.onCloseButtonClick(); + return; + case ACTION_MOVE: + mListener.onEnterMoveMode(); + return; + case ACTION_EXPAND_COLLAPSE: + mListener.onToggleExpandedMode(); + return; + case ACTION_CUSTOM: + action.executePendingIntent(); + return; + default: + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: No action available", TAG); + } + } + } + } + interface Listener extends TvPipMenuEduTextDrawer.Listener { void onBackPress(); @@ -730,4 +667,4 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { void onToggleExpandedMode(); } -} +} \ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java new file mode 100644 index 0000000000000..83a26c351bcfe --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 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.annotation.DrawableRes; +import android.annotation.NonNull; +import android.annotation.StringRes; +import android.app.PendingIntent; +import android.os.Handler; + +import com.android.wm.shell.common.TvWindowMenuActionButton; + +/** + * A TvPipAction for actions that the system provides, i.e. fullscreen, default close, move, + * expand/collapse. + */ +public class TvPipSystemAction extends TvPipAction { + + @StringRes + private int mTitleResource; + @DrawableRes + private int mIconResource; + + TvPipSystemAction(@ActionType int actionType, @StringRes int title, @DrawableRes int icon) { + super(actionType); + update(title, icon); + } + + void update(@StringRes int title, @DrawableRes int icon) { + mTitleResource = title; + mIconResource = icon; + } + + void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler) { + button.setTextAndDescription(mTitleResource); + button.setImageResource(mIconResource); + button.setEnabled(true); + } + + PendingIntent getPendingIntent() { + return null; + } + +} From 5e56e0c0211f0e896aaf3428ca4849e1940c4080 Mon Sep 17 00:00:00 2001 From: Jacqueline Bronger Date: Tue, 8 Nov 2022 17:29:48 +0100 Subject: [PATCH 2/3] Handle all TvPipActions within one component. Introduces a TvPipActionsProvider that keeps the list of all the pip actions that should be displayed. It gets updates about the PiP from the TvPipController and sends changes to the actions to its listeners (TvPipMenuView and TvPipNotificationController). Bug: 258653494 Test: atest TvPipActionProviderTest Test: manual - check PiP menu content is populated Change-Id: Ibf892d7d3fb3efc1182faabca68b9ef772c9c606 --- .../android/wm/shell/dagger/TvPipModule.java | 20 +- .../android/wm/shell/pip/tv/TvPipAction.java | 4 + .../wm/shell/pip/tv/TvPipActionsProvider.java | 232 +++++++++++++ .../wm/shell/pip/tv/TvPipController.java | 26 +- .../wm/shell/pip/tv/TvPipCustomAction.java | 23 ++ .../wm/shell/pip/tv/TvPipMenuController.java | 77 +---- .../wm/shell/pip/tv/TvPipMenuView.java | 137 ++------ .../pip/tv/TvPipNotificationController.java | 205 +++--------- .../wm/shell/pip/tv/TvPipSystemAction.java | 26 +- .../shell/pip/tv/TvPipActionProviderTest.java | 306 ++++++++++++++++++ 10 files changed, 699 insertions(+), 357 deletions(-) create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java create mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java 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 8022e9b1cd810..d64d92b36335b 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 @@ -39,6 +39,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.TvPipActionsProvider; import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm; import com.android.wm.shell.pip.tv.TvPipBoundsController; import com.android.wm.shell.pip.tv.TvPipBoundsState; @@ -75,6 +76,7 @@ public abstract class TvPipModule { PipTaskOrganizer pipTaskOrganizer, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, + TvPipActionsProvider tvPipActionsProvider, PipTransitionController pipTransitionController, TvPipNotificationController tvPipNotificationController, TaskStackListenerImpl taskStackListener, @@ -95,6 +97,7 @@ public abstract class TvPipModule { pipTransitionController, tvPipMenuController, pipMediaController, + tvPipActionsProvider, tvPipNotificationController, taskStackListener, pipParamsChangedForwarder, @@ -157,10 +160,10 @@ public abstract class TvPipModule { Context context, TvPipBoundsState tvPipBoundsState, SystemWindows systemWindows, - PipMediaController pipMediaController, + TvPipActionsProvider tvPipActionsProvider, @ShellMainThread Handler mainHandler) { - return new TvPipMenuController(context, tvPipBoundsState, systemWindows, pipMediaController, - mainHandler); + return new TvPipMenuController(context, tvPipBoundsState, systemWindows, mainHandler, + tvPipActionsProvider); } // Handler needed for registerReceiverForAllUsers() @@ -169,10 +172,10 @@ public abstract class TvPipModule { static TvPipNotificationController provideTvPipNotificationController(Context context, PipMediaController pipMediaController, PipParamsChangedForwarder pipParamsChangedForwarder, - TvPipBoundsState tvPipBoundsState, + TvPipActionsProvider tvPipActionsProvider, @ShellMainThread Handler mainHandler) { return new TvPipNotificationController(context, pipMediaController, - pipParamsChangedForwarder, tvPipBoundsState, mainHandler); + pipParamsChangedForwarder, tvPipActionsProvider, mainHandler); } @WMSingleton @@ -224,4 +227,11 @@ public abstract class TvPipModule { @ShellMainThread ShellExecutor mainExecutor) { return new PipAppOpsListener(context, pipTaskOrganizer::removePip, mainExecutor); } + + @WMSingleton + @Provides + static TvPipActionsProvider provideTvPipActionsProvider(Context context, + PipMediaController pipMediaController) { + return new TvPipActionsProvider(context, pipMediaController); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java index 9f8f95c93fec2..1364229082025 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java @@ -18,7 +18,9 @@ package com.android.wm.shell.pip.tv; import android.annotation.IntDef; import android.annotation.NonNull; +import android.app.Notification; import android.app.PendingIntent; +import android.content.Context; import android.os.Handler; import com.android.internal.protolog.common.ProtoLog; @@ -81,4 +83,6 @@ abstract class TvPipAction { } } + abstract Notification.Action toNotificationAction(Context context); + } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java new file mode 100644 index 0000000000000..2214ad166220b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2022 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 static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; +import static com.android.wm.shell.pip.tv.TvPipNotificationController.ACTION_CLOSE_PIP; +import static com.android.wm.shell.pip.tv.TvPipNotificationController.ACTION_MOVE_PIP; +import static com.android.wm.shell.pip.tv.TvPipNotificationController.ACTION_TOGGLE_EXPANDED_PIP; +import static com.android.wm.shell.pip.tv.TvPipNotificationController.ACTION_TO_FULLSCREEN; + +import android.annotation.NonNull; +import android.app.RemoteAction; +import android.content.Context; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipUtils; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * Creates the system TvPipActions (fullscreen, close, move, expand/collapse), and handles all the + * changes to the actions, including the custom app actions and media actions. Other components can + * listen to those changes. + */ +public class TvPipActionsProvider { + private static final String TAG = TvPipActionsProvider.class.getSimpleName(); + + private static final int CLOSE_ACTION_INDEX = 1; + private static final int FIRST_CUSTOM_ACTION_INDEX = 2; + + private final List mListeners = new ArrayList<>(); + + private final List mActionsList; + private final TvPipSystemAction mDefaultCloseAction; + private final TvPipSystemAction mExpandCollapseAction; + + private final List mMediaActions = new ArrayList<>(); + private final List mAppActions = new ArrayList<>(); + + public TvPipActionsProvider(Context context, PipMediaController pipMediaController) { + + mActionsList = new ArrayList<>(); + mActionsList.add(new TvPipSystemAction(ACTION_FULLSCREEN, R.string.pip_fullscreen, + R.drawable.pip_ic_fullscreen_white, ACTION_TO_FULLSCREEN, context)); + + mDefaultCloseAction = new TvPipSystemAction(ACTION_CLOSE, R.string.pip_close, + R.drawable.pip_ic_close_white, ACTION_CLOSE_PIP, context); + mActionsList.add(mDefaultCloseAction); + + mActionsList.add(new TvPipSystemAction(ACTION_MOVE, R.string.pip_move, + R.drawable.pip_ic_move_white, ACTION_MOVE_PIP, context)); + + mExpandCollapseAction = new TvPipSystemAction(ACTION_EXPAND_COLLAPSE, R.string.pip_collapse, + R.drawable.pip_ic_collapse, ACTION_TOGGLE_EXPANDED_PIP, context); + mActionsList.add(mExpandCollapseAction); + + pipMediaController.addActionListener(this::onMediaActionsChanged); + } + + private void notifyActionsChanged(int added, int changed, int startIndex) { + for (Listener listener : mListeners) { + listener.onActionsChanged(added, changed, startIndex); + } + } + + @VisibleForTesting(visibility = PACKAGE) + public void setAppActions(@NonNull List appActions, RemoteAction closeAction) { + // Update close action. + mActionsList.set(CLOSE_ACTION_INDEX, + closeAction == null ? mDefaultCloseAction + : new TvPipCustomAction(ACTION_CUSTOM_CLOSE, closeAction)); + notifyActionsChanged(/* added= */ 0, /* updated= */ 1, CLOSE_ACTION_INDEX); + + // Replace custom actions with new ones. + mAppActions.clear(); + for (RemoteAction action : appActions) { + if (action != null && !PipUtils.remoteActionsMatch(action, closeAction)) { + // Only show actions that aren't duplicates of the custom close action. + mAppActions.add(action); + } + } + + updateCustomActions(mAppActions); + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public void onMediaActionsChanged(List actions) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onMediaActionsChanged()", TAG); + + mMediaActions.clear(); + // Don't show disabled actions. + for (RemoteAction remoteAction : actions) { + if (remoteAction.isEnabled()) { + mMediaActions.add(remoteAction); + } + } + + updateCustomActions(mMediaActions); + } + + private void updateCustomActions(@NonNull List customActions) { + List newCustomActions = customActions; + if (newCustomActions == mMediaActions && !mAppActions.isEmpty()) { + // Don't show the media actions while there are app actions. + return; + } else if (newCustomActions == mAppActions && mAppActions.isEmpty()) { + // If all the app actions were removed, show the media actions. + newCustomActions = mMediaActions; + } + + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: replaceCustomActions, count: %d", TAG, newCustomActions.size()); + int oldCustomActionsCount = 0; + for (TvPipAction action : mActionsList) { + if (action.getActionType() == ACTION_CUSTOM) { + oldCustomActionsCount++; + } + } + mActionsList.removeIf(tvPipAction -> tvPipAction.getActionType() == ACTION_CUSTOM); + + List actions = new ArrayList<>(); + for (RemoteAction action : newCustomActions) { + actions.add(new TvPipCustomAction(ACTION_CUSTOM, action)); + } + mActionsList.addAll(FIRST_CUSTOM_ACTION_INDEX, actions); + + int added = newCustomActions.size() - oldCustomActionsCount; + int changed = Math.min(newCustomActions.size(), oldCustomActionsCount); + notifyActionsChanged(added, changed, FIRST_CUSTOM_ACTION_INDEX); + } + + @VisibleForTesting(visibility = PACKAGE) + public void updateExpansionEnabled(boolean enabled) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: updateExpansionState, enabled: %b", TAG, enabled); + int actionIndex = mActionsList.indexOf(mExpandCollapseAction); + boolean actionInList = actionIndex != -1; + if (enabled && !actionInList) { + mActionsList.add(mExpandCollapseAction); + actionIndex = mActionsList.size() - 1; + } else if (!enabled && actionInList) { + mActionsList.remove(actionIndex); + } else { + return; + } + notifyActionsChanged(/* added= */ enabled ? 1 : -1, /* updated= */ 0, actionIndex); + } + + @VisibleForTesting(visibility = PACKAGE) + public void onPipExpansionToggled(boolean expanded) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipExpansionToggled, expanded: %b", TAG, expanded); + + mExpandCollapseAction.update( + expanded ? R.string.pip_collapse : R.string.pip_expand, + expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand); + + notifyActionsChanged(/* added= */ 0, /* updated= */ 1, + mActionsList.indexOf(mExpandCollapseAction)); + } + + List getActionsList() { + return mActionsList; + } + + @NonNull + TvPipAction getCloseAction() { + return mActionsList.get(CLOSE_ACTION_INDEX); + } + + void addListener(Listener listener) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + } + } + + /** + * Returns the index of the first action of the given action type or -1 if none can be found. + */ + int getFirstIndexOfAction(@TvPipAction.ActionType int actionType) { + for (int i = 0; i < mActionsList.size(); i++) { + if (mActionsList.get(i).getActionType() == actionType) { + return i; + } + } + return -1; + } + + /** + * Allow components to listen to updates to the actions list, including where they happen so + * that changes can be animated. + */ + interface Listener { + /** + * Notifies the listener how many actions were added/removed or updated. + * + * @param added can be positive (number of actions added), negative (number of actions + * removed) or zero (the number of actions stayed the same). + * @param updated the number of actions that might have been updated and need to be + * refreshed. + * @param startIndex The index of the first updated action. The added/removed actions start + * at (startIndex + updated). + */ + void onActionsChanged(int added, int updated, int startIndex); + } +} 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 3e8de454bcff6..f5691c5b112c2 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 @@ -107,6 +107,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private final PipAppOpsListener mAppOpsListener; private final PipTaskOrganizer mPipTaskOrganizer; private final PipMediaController mPipMediaController; + private final TvPipActionsProvider mTvPipActionsProvider; private final TvPipNotificationController mPipNotificationController; private final TvPipMenuController mTvPipMenuController; private final PipTransitionController mPipTransitionController; @@ -141,6 +142,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, + TvPipActionsProvider tvPipActionsProvider, TvPipNotificationController pipNotificationController, TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, @@ -159,6 +161,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal pipTransitionController, tvPipMenuController, pipMediaController, + tvPipActionsProvider, pipNotificationController, taskStackListener, pipParamsChangedForwarder, @@ -179,6 +182,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, + TvPipActionsProvider tvPipActionsProvider, TvPipNotificationController pipNotificationController, TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, @@ -198,6 +202,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mTvPipBoundsController.setListener(this); mPipMediaController = pipMediaController; + mTvPipActionsProvider = tvPipActionsProvider; mPipNotificationController = pipNotificationController; mPipNotificationController.setDelegate(this); @@ -241,7 +246,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState)); loadConfigurations(); - mPipNotificationController.onConfigurationChanged(mContext); + mPipNotificationController.onConfigurationChanged(); mTvPipBoundsAlgorithm.onConfigurationChanged(mContext); } @@ -310,7 +315,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding); mTvPipBoundsState.setTvPipExpanded(expanding); - mPipNotificationController.updateExpansionState(); updatePinnedStackBounds(); } @@ -373,7 +377,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) { mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds, - animationDuration, rect -> mTvPipMenuController.updateExpansionState()); + animationDuration, null); mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds); } @@ -454,6 +458,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void onPipTransitionStarted(int direction, Rect currentPipBounds) { + final boolean enterPipTransition = PipAnimationController.isInPipDirection(direction); + if (enterPipTransition && mState == STATE_NO_PIP) { + // Set the initial ability to expand the PiP when entering PiP. + updateExpansionState(); + } ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransition_Started(), state=%s, direction=%d", TAG, stateToName(mState), direction); @@ -465,6 +474,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState)); mTvPipMenuController.onPipTransitionFinished( PipAnimationController.isInPipDirection(direction)); + mTvPipActionsProvider.onPipExpansionToggled(mTvPipBoundsState.isTvPipExpanded()); } @Override @@ -477,6 +487,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: onPipTransition_Finished(), state=%s, direction=%d", TAG, stateToName(mState), direction); mTvPipMenuController.onPipTransitionFinished(enterPipTransition); + mTvPipActionsProvider.onPipExpansionToggled(mTvPipBoundsState.isTvPipExpanded()); + } + + private void updateExpansionState() { + mTvPipActionsProvider.updateExpansionEnabled(mTvPipBoundsState.isTvExpandedPipSupported() + && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0); } private void setState(@State int state) { @@ -534,7 +550,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onActionsChanged()", TAG); - mTvPipMenuController.setAppActions(actions, closeAction); + mTvPipActionsProvider.setAppActions(actions, closeAction); mCloseAction = closeAction; } @@ -555,7 +571,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: onExpandedAspectRatioChanged: %f", TAG, ratio); mTvPipBoundsState.setDesiredTvExpandedAspectRatio(ratio, false); - mTvPipMenuController.updateExpansionState(); + updateExpansionState(); // 1) PiP is expanded and only aspect ratio changed, but wasn't disabled // --> update bounds, but don't toggle diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java index 5c0b1b3aa383d..4bd240afb5638 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java @@ -16,9 +16,15 @@ package com.android.wm.shell.pip.tv; +import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE; +import static android.app.Notification.Action.SEMANTIC_ACTION_NONE; + import android.annotation.NonNull; +import android.app.Notification; import android.app.PendingIntent; import android.app.RemoteAction; +import android.content.Context; +import android.os.Bundle; import android.os.Handler; import com.android.wm.shell.common.TvWindowMenuActionButton; @@ -56,4 +62,21 @@ public class TvPipCustomAction extends TvPipAction { return mRemoteAction.getActionIntent(); } + @Override + Notification.Action toNotificationAction(Context context) { + Notification.Action.Builder builder = new Notification.Action.Builder( + mRemoteAction.getIcon(), + mRemoteAction.getTitle(), + mRemoteAction.getActionIntent()); + Bundle extras = new Bundle(); + extras.putCharSequence(Notification.EXTRA_PICTURE_CONTENT_DESCRIPTION, + mRemoteAction.getContentDescription()); + builder.addExtras(extras); + + builder.setSemanticAction(isCloseAction() + ? SEMANTIC_ACTION_DELETE : SEMANTIC_ACTION_NONE); + builder.setContextual(true); + return builder.build(); + } + } 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 e80602ea0cd92..bcb995ae8a090 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 @@ -41,13 +41,10 @@ import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.SystemWindows; -import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.ArrayList; import java.util.List; -import java.util.Objects; /** * Manages the visibility of the PiP Menu as user interacts with PiP. @@ -60,6 +57,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private final SystemWindows mSystemWindows; private final TvPipBoundsState mTvPipBoundsState; private final Handler mMainHandler; + private final TvPipActionsProvider mTvPipActionsProvider; private Delegate mDelegate; private SurfaceControl mLeash; @@ -72,10 +70,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis // exiting the move menu instead of showing the regular button menu. private boolean mCloseAfterExitMoveMenu; - private final List mMediaActions = new ArrayList<>(); - private final List mAppActions = new ArrayList<>(); - private RemoteAction mCloseAction; - private SyncRtSurfaceTransactionApplier mApplier; private SyncRtSurfaceTransactionApplier mBackgroundApplier; RectF mTmpSourceRectF = new RectF(); @@ -83,12 +77,13 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis Matrix mMoveTransform = new Matrix(); public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState, - SystemWindows systemWindows, PipMediaController pipMediaController, - Handler mainHandler) { + SystemWindows systemWindows, Handler mainHandler, + TvPipActionsProvider tvPipActionsProvider) { mContext = context; mTvPipBoundsState = tvPipBoundsState; mSystemWindows = systemWindows; mMainHandler = mainHandler; + mTvPipActionsProvider = tvPipActionsProvider; // We need to "close" the menu the platform call for all the system dialogs to close (for // example, on the Home button press). @@ -101,9 +96,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis context.registerReceiverForAllUsers(closeSystemDialogsBroadcastReceiver, new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), null /* permission */, mainHandler, Context.RECEIVER_EXPORTED); - - pipMediaController.addActionListener(this::onMediaActionsChanged); - } void setDelegate(Delegate delegate) { @@ -151,10 +143,9 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } private void attachPipMenuView() { - mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this); + mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this, mTvPipActionsProvider); setUpViewSurfaceZOrder(mPipMenuView, 1); addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE); - maybeUpdateMenuViewActions(); } private void attachPipBackgroundView() { @@ -189,8 +180,9 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis // and the menu view has been fully remeasured and relaid out, we add a small delay here by // posting on the handler. mMainHandler.post(() -> { - mPipMenuView.onPipTransitionFinished( - enterTransition, mTvPipBoundsState.isTvPipExpanded()); + if (mPipMenuView != null) { + mPipMenuView.onPipTransitionFinished(enterTransition); + } }); } @@ -214,8 +206,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (mPipMenuView == null) { return; } - maybeUpdateMenuViewActions(); - updateExpansionState(); grantPipMenuFocus(true); if (mInMoveMode) { @@ -236,11 +226,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis mPipMenuView.showMovementHints(gravity); } - void updateExpansionState() { - mPipMenuView.setExpandedModeEnabled(mTvPipBoundsState.isTvExpandedPipSupported() - && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0); - } - private Rect calculateMenuSurfaceBounds(Rect pipBounds) { return mPipMenuView.getPipMenuContainerBounds(pipBounds); } @@ -319,51 +304,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis @Override public void setAppActions(List actions, RemoteAction closeAction) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setAppActions()", TAG); - updateAdditionalActionsList(mAppActions, actions, closeAction); - } - - private void onMediaActionsChanged(List actions) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onMediaActionsChanged()", TAG); - - // Hide disabled actions. - List enabledActions = new ArrayList<>(); - for (RemoteAction remoteAction : actions) { - if (remoteAction.isEnabled()) { - enabledActions.add(remoteAction); - } - } - updateAdditionalActionsList(mMediaActions, enabledActions, mCloseAction); - } - - private void updateAdditionalActionsList(List destination, - @Nullable List source, RemoteAction closeAction) { - final int number = source != null ? source.size() : 0; - if (number == 0 && destination.isEmpty() && Objects.equals(closeAction, mCloseAction)) { - // Nothing changed. - return; - } - - mCloseAction = closeAction; - - destination.clear(); - if (number > 0) { - destination.addAll(source); - } - maybeUpdateMenuViewActions(); - } - - private void maybeUpdateMenuViewActions() { - if (mPipMenuView == null) { - return; - } - if (!mAppActions.isEmpty()) { - mPipMenuView.setAdditionalActions(mAppActions, mCloseAction); - } else { - mPipMenuView.setAdditionalActions(mMediaActions, mCloseAction); - } + // NOOP - handled via the TvPipActionsProvider } @Override 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 d8dc022ad59c2..11ad290dfc2c2 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 @@ -32,7 +32,6 @@ import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE; import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN; import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; -import android.app.RemoteAction; import android.content.Context; import android.graphics.Rect; import android.os.Handler; @@ -54,25 +53,18 @@ import com.android.wm.shell.common.TvWindowMenuActionButton; import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.ArrayList; import java.util.List; /** - * 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, RemoteAction, Handler)} call. + * A View that represents Pip Menu on TV. It's responsible for displaying the Pip menu actions from + * the TvPipActionsProvider as well as the buttons for manually moving the PiP. */ -public class TvPipMenuView extends FrameLayout { +public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.Listener { private static final String TAG = "TvPipMenuView"; - private static final int CLOSE_ACTION_INDEX = 1; - private static final int FIRST_CUSTOM_ACTION_INDEX = 2; - private final TvPipMenuView.Listener mListener; - private final List mActionsList; - private final TvPipSystemAction mDefaultCloseAction; - private final TvPipSystemAction mExpandCollapseAction; + private final TvPipActionsProvider mTvPipActionsProvider; private final RecyclerView mActionButtonsRecyclerView; private final LinearLayoutManager mButtonLayoutManager; @@ -108,7 +100,7 @@ public class TvPipMenuView extends FrameLayout { private final Handler mMainHandler; public TvPipMenuView(@NonNull Context context, @NonNull Handler mainHandler, - @NonNull Listener listener) { + @NonNull Listener listener, TvPipActionsProvider tvPipActionsProvider) { super(context, null, 0, 0); inflate(context, R.layout.tv_pip_menu, this); @@ -121,26 +113,12 @@ public class TvPipMenuView extends FrameLayout { mActionButtonsRecyclerView.setLayoutManager(mButtonLayoutManager); mActionButtonsRecyclerView.setPreserveFocusAfterLayout(true); - mDefaultCloseAction = - new TvPipSystemAction(ACTION_CLOSE, R.string.pip_close, - R.drawable.pip_ic_close_white); - mExpandCollapseAction = - new TvPipSystemAction(ACTION_EXPAND_COLLAPSE, R.string.pip_collapse, - R.drawable.pip_ic_collapse); - - mActionsList = new ArrayList<>(); - mActionsList.add( - new TvPipSystemAction(ACTION_FULLSCREEN, R.string.pip_fullscreen, - R.drawable.pip_ic_fullscreen_white)); - mActionsList.add(mDefaultCloseAction); - mActionsList.add( - new TvPipSystemAction(ACTION_MOVE, R.string.pip_move, - R.drawable.pip_ic_move_white)); - mActionsList.add(mExpandCollapseAction); - - mRecyclerViewAdapter = new RecyclerViewAdapter(mActionsList); + mTvPipActionsProvider = tvPipActionsProvider; + mRecyclerViewAdapter = new RecyclerViewAdapter(tvPipActionsProvider.getActionsList()); mActionButtonsRecyclerView.setAdapter(mRecyclerViewAdapter); + tvPipActionsProvider.addListener(this); + mMenuFrameView = findViewById(R.id.tv_pip_menu_frame); mPipFrameView = findViewById(R.id.tv_pip_border); mPipView = findViewById(R.id.tv_pip); @@ -217,7 +195,7 @@ public class TvPipMenuView extends FrameLayout { } } - void onPipTransitionFinished(boolean enterTransition, boolean isTvPipExpanded) { + void onPipTransitionFinished(boolean enterTransition) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransitionFinished()", TAG); @@ -232,9 +210,6 @@ public class TvPipMenuView extends FrameLayout { mEduTextDrawer.init(); } - // Update buttons. - setIsExpanded(isTvPipExpanded); - if (mSwitchingOrientation) { mActionButtonsRecyclerView.animate() .alpha(1) @@ -294,26 +269,6 @@ public class TvPipMenuView extends FrameLayout { } } - void setExpandedModeEnabled(boolean enabled) { - int actionIndex = mActionsList.indexOf(mExpandCollapseAction); - boolean actionInList = actionIndex != -1; - if (enabled && !actionInList) { - mActionsList.add(mExpandCollapseAction); - mRecyclerViewAdapter.notifyItemInserted(mActionsList.size() - 1); - } else if (!enabled && actionInList) { - mActionsList.remove(actionIndex); - mRecyclerViewAdapter.notifyItemRemoved(actionIndex); - } - } - - void setIsExpanded(boolean expanded) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setIsExpanded, expanded: %b", TAG, expanded); - mExpandCollapseAction.update(expanded ? R.string.pip_collapse : R.string.pip_expand, - expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand); - mRecyclerViewAdapter.notifyItemChanged(mActionsList.indexOf(mExpandCollapseAction)); - } - /** * @param gravity for the arrow hints */ @@ -339,7 +294,7 @@ public class TvPipMenuView extends FrameLayout { mEduTextDrawer.closeIfNeeded(); if (exitingMoveMode) { - scrollAndRefocusButton(getFirstIndexOfAction(ACTION_MOVE), + scrollAndRefocusButton(mTvPipActionsProvider.getFirstIndexOfAction(ACTION_MOVE), /* alwaysScroll= */ false); } else { scrollAndRefocusButton(0, /* alwaysScroll= */ true); @@ -369,18 +324,6 @@ public class TvPipMenuView extends FrameLayout { return itemToFocus != null; } - /** - * Returns the position of the first action of the given action type or -1 if none can be found. - */ - private int getFirstIndexOfAction(@TvPipAction.ActionType int actionType) { - for (int i = 0; i < mActionsList.size(); i++) { - if (mActionsList.get(i).getActionType() == actionType) { - return i; - } - } - return -1; - } - void hideAllUserControls() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: hideAllUserControls()", TAG); @@ -418,57 +361,13 @@ public class TvPipMenuView extends FrameLayout { }); } - void setAdditionalActions(List actions, RemoteAction closeAction) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setAdditionalActions(), %d actions", TAG, actions.size()); - - int oldCustomActionCount = 0; - for (TvPipAction action : mActionsList) { - if (action.getActionType() == ACTION_CUSTOM) { - oldCustomActionCount++; - } - } - - // Update close action. - mActionsList.set(CLOSE_ACTION_INDEX, - closeAction == null ? mDefaultCloseAction - : new TvPipCustomAction(ACTION_CUSTOM_CLOSE, closeAction)); - mRecyclerViewAdapter.notifyItemChanged(CLOSE_ACTION_INDEX); - - // Replace custom actions with new ones. - mActionsList.removeIf(tvPipAction -> tvPipAction.getActionType() == ACTION_CUSTOM); - List customActions = new ArrayList<>(actions.size()); - int newCustomActionCount = 0; - for (RemoteAction action : actions) { - if (action == null || PipUtils.remoteActionsMatch(action, closeAction)) { - // Don't show an action if it is the same as the custom close action - continue; - } - customActions.add(new TvPipCustomAction(ACTION_CUSTOM, action)); - newCustomActionCount++; - } - mActionsList.addAll(FIRST_CUSTOM_ACTION_INDEX, customActions); - - mRecyclerViewAdapter.notifyItemRangeChanged( - FIRST_CUSTOM_ACTION_INDEX, Math.min(oldCustomActionCount, newCustomActionCount)); - - if (newCustomActionCount > oldCustomActionCount) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setAdditionalActions(), %d inserted starting at %d", - TAG, newCustomActionCount - oldCustomActionCount, - FIRST_CUSTOM_ACTION_INDEX + oldCustomActionCount); - mRecyclerViewAdapter.notifyItemRangeInserted( - FIRST_CUSTOM_ACTION_INDEX + oldCustomActionCount, - newCustomActionCount - oldCustomActionCount); - - } else if (oldCustomActionCount > newCustomActionCount) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setAdditionalActions(), %d removed starting at %d", - TAG, oldCustomActionCount - newCustomActionCount, - FIRST_CUSTOM_ACTION_INDEX + newCustomActionCount); - mRecyclerViewAdapter.notifyItemRangeRemoved( - FIRST_CUSTOM_ACTION_INDEX + newCustomActionCount, - oldCustomActionCount - newCustomActionCount); + @Override + public void onActionsChanged(int added, int updated, int startIndex) { + mRecyclerViewAdapter.notifyItemRangeChanged(startIndex, updated); + if (added > 0) { + mRecyclerViewAdapter.notifyItemRangeInserted(startIndex + updated, added); + } else if (added < 0) { + mRecyclerViewAdapter.notifyItemRangeRemoved(startIndex + updated, -added); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java index e3308f0763a04..848d591f08a32 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java @@ -16,13 +16,10 @@ package com.android.wm.shell.pip.tv; -import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE; -import static android.app.Notification.Action.SEMANTIC_ACTION_NONE; - +import android.annotation.NonNull; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; -import android.app.RemoteAction; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -47,7 +44,6 @@ import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.ArrayList; import java.util.List; /** @@ -55,7 +51,7 @@ import java.util.List; *

Once it's created, it will manage the PiP notification UI by itself except for handling * configuration changes and user initiated expanded PiP toggling. */ -public class TvPipNotificationController { +public class TvPipNotificationController implements TvPipActionsProvider.Listener { private static final String TAG = "TvPipNotification"; // Referenced in com.android.systemui.util.NotificationChannels. @@ -65,13 +61,13 @@ public class TvPipNotificationController { private static final String ACTION_SHOW_PIP_MENU = "com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU"; - private static final String ACTION_CLOSE_PIP = + static final String ACTION_CLOSE_PIP = "com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP"; - private static final String ACTION_MOVE_PIP = + static final String ACTION_MOVE_PIP = "com.android.wm.shell.pip.tv.notification.action.MOVE_PIP"; - private static final String ACTION_TOGGLE_EXPANDED_PIP = + static final String ACTION_TOGGLE_EXPANDED_PIP = "com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP"; - private static final String ACTION_FULLSCREEN = + static final String ACTION_TO_FULLSCREEN = "com.android.wm.shell.pip.tv.notification.action.FULLSCREEN"; private final Context mContext; @@ -81,13 +77,7 @@ public class TvPipNotificationController { private final ActionBroadcastReceiver mActionBroadcastReceiver; private final Handler mMainHandler; private Delegate mDelegate; - private final TvPipBoundsState mTvPipBoundsState; - - private String mDefaultTitle; - - private final List mCustomActions = new ArrayList<>(); - private final List mMediaActions = new ArrayList<>(); - private RemoteAction mCustomCloseAction; + private final TvPipActionsProvider mTvPipActionsProvider; private MediaSession.Token mMediaSessionToken; @@ -95,19 +85,28 @@ public class TvPipNotificationController { private String mPackageName; private boolean mIsNotificationShown; + private String mDefaultTitle; private String mPipTitle; private String mPipSubtitle; + // Saving the actions so they don't have to be regenerated when e.g. the PiP title changes. + @NonNull + private Notification.Action[] mPipActions; + private Bitmap mActivityIcon; public TvPipNotificationController(Context context, PipMediaController pipMediaController, - PipParamsChangedForwarder pipParamsChangedForwarder, TvPipBoundsState tvPipBoundsState, - Handler mainHandler) { + PipParamsChangedForwarder pipParamsChangedForwarder, + TvPipActionsProvider tvPipActionsProvider, Handler mainHandler) { mContext = context; mPackageManager = context.getPackageManager(); mNotificationManager = context.getSystemService(NotificationManager.class); mMainHandler = mainHandler; - mTvPipBoundsState = tvPipBoundsState; + + mTvPipActionsProvider = tvPipActionsProvider; + mTvPipActionsProvider.addListener(this); + + mPipActions = new Notification.Action[0]; mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL) .setLocalOnly(true) @@ -117,33 +116,15 @@ public class TvPipNotificationController { .setOnlyAlertOnce(true) .setSmallIcon(R.drawable.pip_icon) .setAllowSystemGeneratedContextualActions(false) - .setContentIntent(createPendingIntent(context, ACTION_FULLSCREEN)) - .setDeleteIntent(getCloseAction().actionIntent) - .extend(new Notification.TvExtender() - .setContentIntent(createPendingIntent(context, ACTION_SHOW_PIP_MENU)) - .setDeleteIntent(createPendingIntent(context, ACTION_CLOSE_PIP))); + .setContentIntent(createPendingIntent(context, ACTION_TO_FULLSCREEN)); + // TvExtender and DeleteIntent set later since they might change. mActionBroadcastReceiver = new ActionBroadcastReceiver(); - pipMediaController.addActionListener(this::onMediaActionsChanged); pipMediaController.addTokenListener(this::onMediaSessionTokenChanged); pipParamsChangedForwarder.addListener( new PipParamsChangedForwarder.PipParamsChangedCallback() { - @Override - public void onExpandedAspectRatioChanged(float ratio) { - updateExpansionState(); - } - - @Override - public void onActionsChanged(List actions, - RemoteAction closeAction) { - mCustomActions.clear(); - mCustomActions.addAll(actions); - mCustomCloseAction = closeAction; - updateNotificationContent(); - } - @Override public void onTitleChanged(String title) { mPipTitle = title; @@ -157,7 +138,12 @@ public class TvPipNotificationController { } }); - onConfigurationChanged(context); + onConfigurationChanged(); + } + + void onConfigurationChanged() { + mDefaultTitle = mContext.getResources().getString(R.string.pip_notification_unknown_title); + updateNotificationContent(); } void setDelegate(Delegate delegate) { @@ -171,7 +157,6 @@ public class TvPipNotificationController { if (delegate == null) { throw new IllegalArgumentException("The delegate must not be null."); } - mDelegate = delegate; } @@ -199,146 +184,38 @@ public class TvPipNotificationController { mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP); } - private Notification.Action getToggleAction(boolean expanded) { - if (expanded) { - return createSystemAction(R.drawable.pip_ic_collapse, - R.string.pip_collapse, ACTION_TOGGLE_EXPANDED_PIP); - } else { - return createSystemAction(R.drawable.pip_ic_expand, R.string.pip_expand, - ACTION_TOGGLE_EXPANDED_PIP); - } - } - - private Notification.Action createSystemAction(int iconRes, int titleRes, String action) { - Notification.Action.Builder builder = new Notification.Action.Builder( - Icon.createWithResource(mContext, iconRes), - mContext.getString(titleRes), - createPendingIntent(mContext, action)); - builder.setContextual(true); - return builder.build(); - } - - private void onMediaActionsChanged(List actions) { - mMediaActions.clear(); - mMediaActions.addAll(actions); - if (mCustomActions.isEmpty()) { - updateNotificationContent(); - } - } - private void onMediaSessionTokenChanged(MediaSession.Token token) { mMediaSessionToken = token; updateNotificationContent(); } - private Notification.Action remoteToNotificationAction(RemoteAction action) { - return remoteToNotificationAction(action, SEMANTIC_ACTION_NONE); - } - - private Notification.Action remoteToNotificationAction(RemoteAction action, - int semanticAction) { - Notification.Action.Builder builder = new Notification.Action.Builder(action.getIcon(), - action.getTitle(), - action.getActionIntent()); - if (action.getContentDescription() != null) { - Bundle extras = new Bundle(); - extras.putCharSequence(Notification.EXTRA_PICTURE_CONTENT_DESCRIPTION, - action.getContentDescription()); - builder.addExtras(extras); - } - builder.setSemanticAction(semanticAction); - builder.setContextual(true); - return builder.build(); - } - - private Notification.Action[] getNotificationActions() { - final List actions = new ArrayList<>(); - - // 1. Fullscreen - actions.add(getFullscreenAction()); - // 2. Close - actions.add(getCloseAction()); - // 3. App actions - final List appActions = - mCustomActions.isEmpty() ? mMediaActions : mCustomActions; - for (RemoteAction appAction : appActions) { - if (PipUtils.remoteActionsMatch(mCustomCloseAction, appAction) - || !appAction.isEnabled()) { - continue; - } - actions.add(remoteToNotificationAction(appAction)); - } - // 4. Move - actions.add(getMoveAction()); - // 5. Toggle expansion (if expanded PiP enabled) - if (mTvPipBoundsState.getDesiredTvExpandedAspectRatio() > 0 - && mTvPipBoundsState.isTvExpandedPipSupported()) { - actions.add(getToggleAction(mTvPipBoundsState.isTvPipExpanded())); - } - return actions.toArray(new Notification.Action[0]); - } - - private Notification.Action getCloseAction() { - if (mCustomCloseAction == null) { - return createSystemAction(R.drawable.pip_ic_close_white, R.string.pip_close, - ACTION_CLOSE_PIP); - } else { - return remoteToNotificationAction(mCustomCloseAction, SEMANTIC_ACTION_DELETE); - } - } - - private Notification.Action getFullscreenAction() { - return createSystemAction(R.drawable.pip_ic_fullscreen_white, - R.string.pip_fullscreen, ACTION_FULLSCREEN); - } - - private Notification.Action getMoveAction() { - return createSystemAction(R.drawable.pip_ic_move_white, R.string.pip_move, - ACTION_MOVE_PIP); - } - - /** - * Called by {@link TvPipController} when the configuration is changed. - */ - void onConfigurationChanged(Context context) { - mDefaultTitle = context.getResources().getString(R.string.pip_notification_unknown_title); - updateNotificationContent(); - } - - void updateExpansionState() { - updateNotificationContent(); - } - private void updateNotificationContent() { if (mPackageManager == null || !mIsNotificationShown) { return; } - Notification.Action[] actions = getNotificationActions(); ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: update(), title: %s, subtitle: %s, mediaSessionToken: %s, #actions: %s", TAG, - getNotificationTitle(), mPipSubtitle, mMediaSessionToken, actions.length); - for (Notification.Action action : actions) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: action: %s", TAG, - action.toString()); - } - + getNotificationTitle(), mPipSubtitle, mMediaSessionToken, mPipActions.length); mNotificationBuilder .setWhen(System.currentTimeMillis()) .setContentTitle(getNotificationTitle()) .setContentText(mPipSubtitle) .setSubText(getApplicationLabel(mPackageName)) - .setActions(actions); + .setActions(mPipActions); setPipIcon(); Bundle extras = new Bundle(); extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, mMediaSessionToken); mNotificationBuilder.setExtras(extras); + PendingIntent closeIntent = mTvPipActionsProvider.getCloseAction().getPendingIntent(); + mNotificationBuilder.setDeleteIntent(closeIntent); // TvExtender not recognized if not set last. mNotificationBuilder.extend(new Notification.TvExtender() .setContentIntent(createPendingIntent(mContext, ACTION_SHOW_PIP_MENU)) - .setDeleteIntent(createPendingIntent(mContext, ACTION_CLOSE_PIP))); + .setDeleteIntent(closeIntent)); + mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP, mNotificationBuilder.build()); } @@ -390,12 +267,22 @@ public class TvPipNotificationController { return ImageUtils.buildScaledBitmap(drawable, width, height, /* allowUpscaling */ true); } - private static PendingIntent createPendingIntent(Context context, String action) { + static PendingIntent createPendingIntent(Context context, String action) { return PendingIntent.getBroadcast(context, 0, new Intent(action).setPackage(context.getPackageName()), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } + @Override + public void onActionsChanged(int added, int updated, int startIndex) { + List actions = mTvPipActionsProvider.getActionsList(); + mPipActions = new Notification.Action[actions.size()]; + for (int i = 0; i < mPipActions.length; i++) { + mPipActions[i] = actions.get(i).toNotificationAction(mContext); + } + updateNotificationContent(); + } + private class ActionBroadcastReceiver extends BroadcastReceiver { final IntentFilter mIntentFilter; { @@ -404,7 +291,7 @@ public class TvPipNotificationController { mIntentFilter.addAction(ACTION_SHOW_PIP_MENU); mIntentFilter.addAction(ACTION_MOVE_PIP); mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP); - mIntentFilter.addAction(ACTION_FULLSCREEN); + mIntentFilter.addAction(ACTION_TO_FULLSCREEN); } boolean mRegistered = false; @@ -437,7 +324,7 @@ public class TvPipNotificationController { mDelegate.enterPipMovementMenu(); } else if (ACTION_TOGGLE_EXPANDED_PIP.equals(action)) { mDelegate.togglePipExpansion(); - } else if (ACTION_FULLSCREEN.equals(action)) { + } else if (ACTION_TO_FULLSCREEN.equals(action)) { mDelegate.movePipToFullscreen(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java index 83a26c351bcfe..8bf5d2a724c24 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java @@ -16,10 +16,16 @@ package com.android.wm.shell.pip.tv; +import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE; +import static android.app.Notification.Action.SEMANTIC_ACTION_NONE; + import android.annotation.DrawableRes; import android.annotation.NonNull; import android.annotation.StringRes; +import android.app.Notification; import android.app.PendingIntent; +import android.content.Context; +import android.graphics.drawable.Icon; import android.os.Handler; import com.android.wm.shell.common.TvWindowMenuActionButton; @@ -35,9 +41,14 @@ public class TvPipSystemAction extends TvPipAction { @DrawableRes private int mIconResource; - TvPipSystemAction(@ActionType int actionType, @StringRes int title, @DrawableRes int icon) { + private final PendingIntent mBroadcastIntent; + + TvPipSystemAction(@ActionType int actionType, @StringRes int title, @DrawableRes int icon, + String broadcastAction, @NonNull Context context) { super(actionType); update(title, icon); + mBroadcastIntent = TvPipNotificationController.createPendingIntent(context, + broadcastAction); } void update(@StringRes int title, @DrawableRes int icon) { @@ -55,4 +66,17 @@ public class TvPipSystemAction extends TvPipAction { return null; } + @Override + Notification.Action toNotificationAction(Context context) { + Notification.Action.Builder builder = new Notification.Action.Builder( + Icon.createWithResource(context, mIconResource), + context.getString(mTitleResource), + mBroadcastIntent); + + builder.setSemanticAction(isCloseAction() + ? SEMANTIC_ACTION_DELETE : SEMANTIC_ACTION_NONE); + builder.setContextual(true); + return builder.build(); + } + } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java new file mode 100644 index 0000000000000..67d1ad76aea58 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2022 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 static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.graphics.drawable.Icon; +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.Log; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.pip.PipMediaController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +/** + * Unit tests for {@link TvPipActionsProvider} + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class TvPipActionProviderTest extends ShellTestCase { + private static final String TAG = TvPipActionProviderTest.class.getSimpleName(); + private TvPipActionsProvider mActionsProvider; + + @Mock + private PipMediaController mMockPipMediaController; + @Mock + private TvPipActionsProvider.Listener mMockListener; + @Mock + private Icon mMockIcon; + @Mock + private PendingIntent mMockPendingIntent; + + private RemoteAction createRemoteAction(int identifier) { + return new RemoteAction(mMockIcon, "" + identifier, "" + identifier, mMockPendingIntent); + } + + private List createRemoteActions(int numberOfActions) { + List actions = new ArrayList<>(); + for (int i = 0; i < numberOfActions; i++) { + actions.add(createRemoteAction(i)); + } + return actions; + } + + private boolean checkActionsMatch(List actions, int[] actionTypes) { + for (int i = 0; i < actions.size(); i++) { + int type = actions.get(i).getActionType(); + if (type != actionTypes[i]) { + Log.e(TAG, "Action at index " + i + ": found " + type + + ", expected " + actionTypes[i]); + return false; + } + } + return true; + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mActionsProvider = new TvPipActionsProvider(mContext, mMockPipMediaController); + } + + @Test + public void defaultSystemActions_regularPip() { + mActionsProvider.updateExpansionEnabled(false); + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE})); + } + + @Test + public void defaultSystemActions_expandedPip() { + mActionsProvider.updateExpansionEnabled(true); + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + } + + @Test + public void expandedPip_enableExpansion_enable() { + // PiP has expanded PiP disabled. + mActionsProvider.updateExpansionEnabled(false); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.updateExpansionEnabled(true); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + verify(mMockListener).onActionsChanged(/* added= */ 1, /* updated= */ 0, /* index= */ 3); + } + + @Test + public void expandedPip_enableExpansion_disable() { + mActionsProvider.updateExpansionEnabled(true); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.updateExpansionEnabled(false); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ -1, /* updated= */ 0, /* index= */ 3); + } + + @Test + public void expandedPip_enableExpansion_AlreadyEnabled() { + mActionsProvider.updateExpansionEnabled(true); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.updateExpansionEnabled(true); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + } + + @Test + public void expandedPip_toggleExpansion() { + // PiP has expanded PiP enabled, but is in a collapsed state + mActionsProvider.updateExpansionEnabled(true); + mActionsProvider.onPipExpansionToggled(/* expanded= */ false); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.onPipExpansionToggled(/* expanded= */ true); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + verify(mMockListener).onActionsChanged(0, 1, 3); + } + + @Test + public void customActions_added() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.addListener(mMockListener); + + mActionsProvider.setAppActions(createRemoteActions(2), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 2, /* updated= */ 0, /* index= */ 2); + } + + @Test + public void customActions_replacedMore() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.setAppActions(createRemoteActions(2), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(3), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_CUSTOM, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 1, /* updated= */ 2, /* index= */ 2); + } + + @Test + public void customActions_replacedLess() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.setAppActions(createRemoteActions(2), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(0), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ -2, /* updated= */ 0, /* index= */ 2); + } + + @Test + public void customCloseAdded() { + mActionsProvider.updateExpansionEnabled(false); + + List customActions = new ArrayList<>(); + mActionsProvider.setAppActions(customActions, null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(customActions, createRemoteAction(0)); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1); + } + + @Test + public void customClose_matchesOtherCustomAction() { + mActionsProvider.updateExpansionEnabled(false); + + List customActions = createRemoteActions(2); + RemoteAction customClose = createRemoteAction(/* id= */ 10); + customActions.add(customClose); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(customActions, customClose); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1); + verify(mMockListener).onActionsChanged(/* added= */ 2, /* updated= */ 0, /* index= */ 2); + } + + @Test + public void mediaActions_added_whileCustomActionsExist() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.setAppActions(createRemoteActions(2), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.onMediaActionsChanged(createRemoteActions(3)); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener, times(0)).onActionsChanged(anyInt(), anyInt(), anyInt()); + } + + @Test + public void customActions_removed_whileMediaActionsExist() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.onMediaActionsChanged(createRemoteActions(2)); + mActionsProvider.setAppActions(createRemoteActions(3), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(0), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ -1, /* updated= */ 2, /* index= */ 2); + } + + @Test + public void customCloseOnly_mediaActionsShowing() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.onMediaActionsChanged(createRemoteActions(2)); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(0), createRemoteAction(5)); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1); + } + + @Test + public void customActions_showDisabledActions() { + mActionsProvider.updateExpansionEnabled(false); + + List customActions = createRemoteActions(2); + customActions.get(0).setEnabled(false); + mActionsProvider.setAppActions(customActions, null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + } + + @Test + public void mediaActions_hideDisabledActions() { + mActionsProvider.updateExpansionEnabled(false); + + List customActions = createRemoteActions(2); + customActions.get(0).setEnabled(false); + mActionsProvider.onMediaActionsChanged(customActions); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_MOVE})); + } + +} + From ff26a3549f32def8a3e118f8c9b3be9218a91f27 Mon Sep 17 00:00:00 2001 From: Jacqueline Bronger Date: Mon, 14 Nov 2022 12:28:06 +0100 Subject: [PATCH 3/3] Unify TvPipAction execution code path. Instead of routing the system actions through the TvPipMenuController and the broadcasts through the TvPipNotificationController, move the BroadcastReceiver into the TvPipController and decide on what system actions to do in there. Bug: 258653494 Test: manual Change-Id: I2b616125cd1b77e333a69b6875337abd5cd0fdf4 --- .../android/wm/shell/dagger/TvPipModule.java | 23 +-- .../android/wm/shell/pip/tv/TvPipAction.java | 25 ++- .../wm/shell/pip/tv/TvPipActionsProvider.java | 37 ++-- .../wm/shell/pip/tv/TvPipController.java | 173 ++++++++++++++---- .../wm/shell/pip/tv/TvPipCustomAction.java | 18 +- .../wm/shell/pip/tv/TvPipMenuController.java | 76 +++----- .../wm/shell/pip/tv/TvPipMenuView.java | 35 +--- .../pip/tv/TvPipNotificationController.java | 128 ++----------- .../wm/shell/pip/tv/TvPipSystemAction.java | 7 +- .../shell/pip/tv/TvPipActionProviderTest.java | 5 +- 10 files changed, 253 insertions(+), 274 deletions(-) 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 d64d92b36335b..b144d22fc3ee4 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 @@ -39,7 +39,6 @@ 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.TvPipActionsProvider; import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm; import com.android.wm.shell.pip.tv.TvPipBoundsController; import com.android.wm.shell.pip.tv.TvPipBoundsState; @@ -76,13 +75,13 @@ public abstract class TvPipModule { PipTaskOrganizer pipTaskOrganizer, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, - TvPipActionsProvider tvPipActionsProvider, PipTransitionController pipTransitionController, TvPipNotificationController tvPipNotificationController, TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, WindowManagerShellWrapper windowManagerShellWrapper, + @ShellMainThread Handler mainHandler, // needed for registerReceiverForAllUsers() @ShellMainThread ShellExecutor mainExecutor) { return Optional.of( TvPipController.create( @@ -97,12 +96,12 @@ public abstract class TvPipModule { pipTransitionController, tvPipMenuController, pipMediaController, - tvPipActionsProvider, tvPipNotificationController, taskStackListener, pipParamsChangedForwarder, displayController, windowManagerShellWrapper, + mainHandler, mainExecutor)); } @@ -160,22 +159,17 @@ public abstract class TvPipModule { Context context, TvPipBoundsState tvPipBoundsState, SystemWindows systemWindows, - TvPipActionsProvider tvPipActionsProvider, @ShellMainThread Handler mainHandler) { - return new TvPipMenuController(context, tvPipBoundsState, systemWindows, mainHandler, - tvPipActionsProvider); + return new TvPipMenuController(context, tvPipBoundsState, systemWindows, mainHandler); } - // Handler needed for registerReceiverForAllUsers() @WMSingleton @Provides static TvPipNotificationController provideTvPipNotificationController(Context context, PipMediaController pipMediaController, - PipParamsChangedForwarder pipParamsChangedForwarder, - TvPipActionsProvider tvPipActionsProvider, - @ShellMainThread Handler mainHandler) { + PipParamsChangedForwarder pipParamsChangedForwarder) { return new TvPipNotificationController(context, pipMediaController, - pipParamsChangedForwarder, tvPipActionsProvider, mainHandler); + pipParamsChangedForwarder); } @WMSingleton @@ -227,11 +221,4 @@ public abstract class TvPipModule { @ShellMainThread ShellExecutor mainExecutor) { return new PipAppOpsListener(context, pipTaskOrganizer::removePip, mainExecutor); } - - @WMSingleton - @Provides - static TvPipActionsProvider provideTvPipActionsProvider(Context context, - PipMediaController pipMediaController) { - return new TvPipActionsProvider(context, pipMediaController); - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java index 1364229082025..222307fba8c29 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java @@ -23,17 +23,14 @@ import android.app.PendingIntent; import android.content.Context; import android.os.Handler; -import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.TvWindowMenuActionButton; -import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Objects; abstract class TvPipAction { - private static final String TAG = TvPipAction.class.getSimpleName(); - @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"ACTION_"}, value = { ACTION_FULLSCREEN, @@ -56,8 +53,13 @@ abstract class TvPipAction { @ActionType private final int mActionType; - TvPipAction(@ActionType int actionType) { + @NonNull + private final SystemActionsHandler mSystemActionsHandler; + + TvPipAction(@ActionType int actionType, @NonNull SystemActionsHandler systemActionsHandler) { + Objects.requireNonNull(systemActionsHandler); mActionType = actionType; + mSystemActionsHandler = systemActionsHandler; } boolean isCloseAction() { @@ -73,16 +75,13 @@ abstract class TvPipAction { abstract PendingIntent getPendingIntent(); - void executePendingIntent() { - if (getPendingIntent() == null) return; - try { - getPendingIntent().send(); - } catch (PendingIntent.CanceledException e) { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Failed to send action, %s", TAG, e); - } + void executeAction() { + mSystemActionsHandler.executeAction(mActionType); } abstract Notification.Action toNotificationAction(Context context); + interface SystemActionsHandler { + void executeAction(@TvPipAction.ActionType int actionType); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java index 2214ad166220b..fa62a73ca9b48 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java @@ -23,10 +23,10 @@ import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE; import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE; import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN; import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; -import static com.android.wm.shell.pip.tv.TvPipNotificationController.ACTION_CLOSE_PIP; -import static com.android.wm.shell.pip.tv.TvPipNotificationController.ACTION_MOVE_PIP; -import static com.android.wm.shell.pip.tv.TvPipNotificationController.ACTION_TOGGLE_EXPANDED_PIP; -import static com.android.wm.shell.pip.tv.TvPipNotificationController.ACTION_TO_FULLSCREEN; +import static com.android.wm.shell.pip.tv.TvPipController.ACTION_CLOSE_PIP; +import static com.android.wm.shell.pip.tv.TvPipController.ACTION_MOVE_PIP; +import static com.android.wm.shell.pip.tv.TvPipController.ACTION_TOGGLE_EXPANDED_PIP; +import static com.android.wm.shell.pip.tv.TvPipController.ACTION_TO_FULLSCREEN; import android.annotation.NonNull; import android.app.RemoteAction; @@ -47,13 +47,14 @@ import java.util.List; * changes to the actions, including the custom app actions and media actions. Other components can * listen to those changes. */ -public class TvPipActionsProvider { +public class TvPipActionsProvider implements TvPipAction.SystemActionsHandler { private static final String TAG = TvPipActionsProvider.class.getSimpleName(); private static final int CLOSE_ACTION_INDEX = 1; private static final int FIRST_CUSTOM_ACTION_INDEX = 2; private final List mListeners = new ArrayList<>(); + private final TvPipAction.SystemActionsHandler mSystemActionsHandler; private final List mActionsList; private final TvPipSystemAction mDefaultCloseAction; @@ -62,26 +63,37 @@ public class TvPipActionsProvider { private final List mMediaActions = new ArrayList<>(); private final List mAppActions = new ArrayList<>(); - public TvPipActionsProvider(Context context, PipMediaController pipMediaController) { + public TvPipActionsProvider(Context context, PipMediaController pipMediaController, + TvPipAction.SystemActionsHandler systemActionsHandler) { + mSystemActionsHandler = systemActionsHandler; mActionsList = new ArrayList<>(); mActionsList.add(new TvPipSystemAction(ACTION_FULLSCREEN, R.string.pip_fullscreen, - R.drawable.pip_ic_fullscreen_white, ACTION_TO_FULLSCREEN, context)); + R.drawable.pip_ic_fullscreen_white, ACTION_TO_FULLSCREEN, context, + mSystemActionsHandler)); mDefaultCloseAction = new TvPipSystemAction(ACTION_CLOSE, R.string.pip_close, - R.drawable.pip_ic_close_white, ACTION_CLOSE_PIP, context); + R.drawable.pip_ic_close_white, ACTION_CLOSE_PIP, context, mSystemActionsHandler); mActionsList.add(mDefaultCloseAction); mActionsList.add(new TvPipSystemAction(ACTION_MOVE, R.string.pip_move, - R.drawable.pip_ic_move_white, ACTION_MOVE_PIP, context)); + R.drawable.pip_ic_move_white, ACTION_MOVE_PIP, context, mSystemActionsHandler)); mExpandCollapseAction = new TvPipSystemAction(ACTION_EXPAND_COLLAPSE, R.string.pip_collapse, - R.drawable.pip_ic_collapse, ACTION_TOGGLE_EXPANDED_PIP, context); + R.drawable.pip_ic_collapse, ACTION_TOGGLE_EXPANDED_PIP, context, + mSystemActionsHandler); mActionsList.add(mExpandCollapseAction); pipMediaController.addActionListener(this::onMediaActionsChanged); } + @Override + public void executeAction(@TvPipAction.ActionType int actionType) { + if (mSystemActionsHandler != null) { + mSystemActionsHandler.executeAction(actionType); + } + } + private void notifyActionsChanged(int added, int changed, int startIndex) { for (Listener listener : mListeners) { listener.onActionsChanged(added, changed, startIndex); @@ -93,7 +105,8 @@ public class TvPipActionsProvider { // Update close action. mActionsList.set(CLOSE_ACTION_INDEX, closeAction == null ? mDefaultCloseAction - : new TvPipCustomAction(ACTION_CUSTOM_CLOSE, closeAction)); + : new TvPipCustomAction(ACTION_CUSTOM_CLOSE, closeAction, + mSystemActionsHandler)); notifyActionsChanged(/* added= */ 0, /* updated= */ 1, CLOSE_ACTION_INDEX); // Replace custom actions with new ones. @@ -146,7 +159,7 @@ public class TvPipActionsProvider { List actions = new ArrayList<>(); for (RemoteAction action : newCustomActions) { - actions.add(new TvPipCustomAction(ACTION_CUSTOM, action)); + actions.add(new TvPipCustomAction(ACTION_CUSTOM, action, mSystemActionsHandler)); } mActionsList.addAll(FIRST_CUSTOM_ACTION_INDEX, actions); 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 f5691c5b112c2..76710818f8e5e 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 @@ -22,13 +22,16 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import android.annotation.IntDef; import android.app.ActivityManager; import android.app.ActivityTaskManager; -import android.app.PendingIntent; import android.app.RemoteAction; import android.app.TaskInfo; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; +import android.os.Handler; import android.os.RemoteException; import android.view.Gravity; @@ -67,8 +70,8 @@ import java.util.Set; */ public class TvPipController implements PipTransitionController.PipTransitionCallback, TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate, - TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener, - ConfigurationChangeListener, UserChangeListener { + DisplayController.OnDisplaysChangedListener, ConfigurationChangeListener, + UserChangeListener { private static final String TAG = "TvPipController"; private static final int NONEXISTENT_TASK_ID = -1; @@ -98,6 +101,17 @@ public class TvPipController implements PipTransitionController.PipTransitionCal */ private static final int STATE_PIP_MENU = 2; + static final String ACTION_SHOW_PIP_MENU = + "com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU"; + static final String ACTION_CLOSE_PIP = + "com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP"; + static final String ACTION_MOVE_PIP = + "com.android.wm.shell.pip.tv.notification.action.MOVE_PIP"; + static final String ACTION_TOGGLE_EXPANDED_PIP = + "com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP"; + static final String ACTION_TO_FULLSCREEN = + "com.android.wm.shell.pip.tv.notification.action.FULLSCREEN"; + private final Context mContext; private final ShellController mShellController; @@ -116,14 +130,16 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private final DisplayController mDisplayController; private final WindowManagerShellWrapper mWmShellWrapper; private final ShellExecutor mMainExecutor; + private final Handler mMainHandler; // For registering the broadcast receiver private final TvPipImpl mImpl = new TvPipImpl(); + private final ActionBroadcastReceiver mActionBroadcastReceiver; + @State private int mState = STATE_NO_PIP; private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY; private int mPinnedTaskId = NONEXISTENT_TASK_ID; - private RemoteAction mCloseAction; // How long the shell will wait for the app to close the PiP if a custom action is set. private int mPipForceCloseDelay; @@ -142,12 +158,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, - TvPipActionsProvider tvPipActionsProvider, TvPipNotificationController pipNotificationController, TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, WindowManagerShellWrapper wmShell, + Handler mainHandler, ShellExecutor mainExecutor) { return new TvPipController( context, @@ -161,12 +177,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal pipTransitionController, tvPipMenuController, pipMediaController, - tvPipActionsProvider, pipNotificationController, taskStackListener, pipParamsChangedForwarder, displayController, wmShell, + mainHandler, mainExecutor).mImpl; } @@ -182,14 +198,15 @@ public class TvPipController implements PipTransitionController.PipTransitionCal PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, - TvPipActionsProvider tvPipActionsProvider, TvPipNotificationController pipNotificationController, TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, WindowManagerShellWrapper wmShellWrapper, + Handler mainHandler, ShellExecutor mainExecutor) { mContext = context; + mMainHandler = mainHandler; mMainExecutor = mainExecutor; mShellController = shellController; mDisplayController = displayController; @@ -202,13 +219,17 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mTvPipBoundsController.setListener(this); mPipMediaController = pipMediaController; - mTvPipActionsProvider = tvPipActionsProvider; + mTvPipActionsProvider = new TvPipActionsProvider(context, pipMediaController, + this::executeAction); mPipNotificationController = pipNotificationController; - mPipNotificationController.setDelegate(this); + mPipNotificationController.setTvPipActionsProvider(mTvPipActionsProvider); mTvPipMenuController = tvPipMenuController; mTvPipMenuController.setDelegate(this); + mTvPipMenuController.setTvPipActionsProvider(mTvPipActionsProvider); + + mActionBroadcastReceiver = new ActionBroadcastReceiver(); mAppOpsListener = pipAppOpsListener; mPipTaskOrganizer = pipTaskOrganizer; @@ -261,9 +282,10 @@ public class TvPipController implements PipTransitionController.PipTransitionCal * Starts the process if bringing up the Pip menu if by issuing a command to move Pip * task/window to the "Menu" position. We'll show the actual Menu UI (eg. actions) once the Pip * task/window is properly positioned in {@link #onPipTransitionFinished(int)}. + * + * @param moveMenu If true, show the moveMenu, otherwise show the regular menu. */ - @Override - public void showPictureInPictureMenu() { + private void showPictureInPictureMenu(boolean moveMenu) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showPictureInPictureMenu(), state=%s", TAG, stateToName(mState)); @@ -274,7 +296,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } setState(STATE_PIP_MENU); - mTvPipMenuController.showMenu(); + if (moveMenu) { + mTvPipMenuController.showMovementMenu(); + } else { + mTvPipMenuController.showMenu(); + } updatePinnedStackBounds(); } @@ -294,8 +320,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal /** * Opens the "Pip-ed" Activity fullscreen. */ - @Override - public void movePipToFullscreen() { + private void movePipToFullscreen() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState)); @@ -303,8 +328,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal onPipDisappeared(); } - @Override - public void togglePipExpansion() { + private void togglePipExpansion() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: togglePipExpansion()", TAG); boolean expanding = !mTvPipBoundsState.isTvPipExpanded(); @@ -319,12 +343,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal updatePinnedStackBounds(); } - @Override - public void enterPipMovementMenu() { - setState(STATE_PIP_MENU); - mTvPipMenuController.showMovementMenuOnly(); - } - @Override public void movePip(int keycode) { if (mTvPipBoundsAlgorithm.updateGravity(keycode)) { @@ -384,23 +402,16 @@ public class TvPipController implements PipTransitionController.PipTransitionCal /** * Closes Pip window. */ - @Override public void closePip() { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: closePip(), state=%s, loseAction=%s", TAG, stateToName(mState), - mCloseAction); + closeCurrentPiP(mPinnedTaskId); + } - if (mCloseAction != null) { - try { - mCloseAction.getActionIntent().send(); - } catch (PendingIntent.CanceledException e) { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Failed to send close action, %s", TAG, e); - } - mMainExecutor.executeDelayed(() -> closeCurrentPiP(mPinnedTaskId), mPipForceCloseDelay); - } else { - closeCurrentPiP(mPinnedTaskId); - } + /** + * Force close the current PiP after some time in case the custom action hasn't done it by + * itself. + */ + public void customClosePip() { + mMainExecutor.executeDelayed(() -> closeCurrentPiP(mPinnedTaskId), mPipForceCloseDelay); } private void closeCurrentPiP(int pinnedTaskId) { @@ -430,6 +441,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mPinnedTaskId = pinnedTask.taskId; mPipMediaController.onActivityPinned(); + mActionBroadcastReceiver.register(); mPipNotificationController.show(pinnedTask.topActivity.getPackageName()); } @@ -449,6 +461,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: onPipDisappeared() state=%s", TAG, stateToName(mState)); mPipNotificationController.dismiss(); + mActionBroadcastReceiver.unregister(); + mTvPipMenuController.closeMenu(); mTvPipBoundsState.resetTvPipState(); mTvPipBoundsController.onPipDismissed(); @@ -551,7 +565,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: onActionsChanged()", TAG); mTvPipActionsProvider.setAppActions(actions, closeAction); - mCloseAction = closeAction; } @Override @@ -678,6 +691,90 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } } + private void executeAction(@TvPipAction.ActionType int actionType) { + switch (actionType) { + case TvPipAction.ACTION_FULLSCREEN: + movePipToFullscreen(); + break; + case TvPipAction.ACTION_CLOSE: + closePip(); + break; + case TvPipAction.ACTION_MOVE: + showPictureInPictureMenu(/* moveMenu= */ true); + break; + case TvPipAction.ACTION_CUSTOM_CLOSE: + customClosePip(); + break; + case TvPipAction.ACTION_EXPAND_COLLAPSE: + togglePipExpansion(); + break; + default: + // NOOP + break; + } + } + + private class ActionBroadcastReceiver extends BroadcastReceiver { + private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF"; + + final IntentFilter mIntentFilter; + + { + mIntentFilter = new IntentFilter(); + mIntentFilter.addAction(ACTION_CLOSE_PIP); + mIntentFilter.addAction(ACTION_SHOW_PIP_MENU); + mIntentFilter.addAction(ACTION_MOVE_PIP); + mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP); + mIntentFilter.addAction(ACTION_TO_FULLSCREEN); + } + + boolean mRegistered = false; + + void register() { + if (mRegistered) return; + + mContext.registerReceiverForAllUsers(this, mIntentFilter, SYSTEMUI_PERMISSION, + mMainHandler); + mRegistered = true; + } + + void unregister() { + if (!mRegistered) return; + + mContext.unregisterReceiver(this); + mRegistered = false; + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: on(Broadcast)Receive(), action=%s", TAG, action); + + if (ACTION_SHOW_PIP_MENU.equals(action)) { + showPictureInPictureMenu(/* moveMenu= */ false); + } else { + executeAction(getCorrespondingActionType(action)); + } + } + + @TvPipAction.ActionType + private int getCorrespondingActionType(String broadcast) { + if (ACTION_CLOSE_PIP.equals(broadcast)) { + return TvPipAction.ACTION_CLOSE; + } else if (ACTION_MOVE_PIP.equals(broadcast)) { + return TvPipAction.ACTION_MOVE; + } else if (ACTION_TOGGLE_EXPANDED_PIP.equals(broadcast)) { + return TvPipAction.ACTION_EXPAND_COLLAPSE; + } else if (ACTION_TO_FULLSCREEN.equals(broadcast)) { + return TvPipAction.ACTION_FULLSCREEN; + } + + // Default: handle it like an action we don't know the content of. + return TvPipAction.ACTION_CUSTOM; + } + } + private class TvPipImpl implements Pip { // Not used } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java index 4bd240afb5638..449a2bf098818 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java @@ -27,7 +27,9 @@ import android.content.Context; import android.os.Bundle; import android.os.Handler; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.TvWindowMenuActionButton; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.List; import java.util.Objects; @@ -38,11 +40,13 @@ import java.util.Objects; * android.app.PictureInPictureParams.Builder#setActions(List)}. */ public class TvPipCustomAction extends TvPipAction { + private static final String TAG = TvPipCustomAction.class.getSimpleName(); private final RemoteAction mRemoteAction; - TvPipCustomAction(@ActionType int actionType, @NonNull RemoteAction remoteAction) { - super(actionType); + TvPipCustomAction(@ActionType int actionType, @NonNull RemoteAction remoteAction, + SystemActionsHandler systemActionsHandler) { + super(actionType, systemActionsHandler); Objects.requireNonNull(remoteAction); mRemoteAction = remoteAction; } @@ -62,6 +66,16 @@ public class TvPipCustomAction extends TvPipAction { return mRemoteAction.getActionIntent(); } + void executeAction() { + super.executeAction(); + try { + mRemoteAction.getActionIntent().send(); + } catch (PendingIntent.CanceledException e) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to send action, %s", TAG, e); + } + } + @Override Notification.Action toNotificationAction(Context context) { Notification.Action.Builder builder = new Notification.Action.Builder( 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 bcb995ae8a090..3233eca4a25cf 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 @@ -57,13 +57,14 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private final SystemWindows mSystemWindows; private final TvPipBoundsState mTvPipBoundsState; private final Handler mMainHandler; - private final TvPipActionsProvider mTvPipActionsProvider; + private TvPipActionsProvider mTvPipActionsProvider; private Delegate mDelegate; private SurfaceControl mLeash; private TvPipMenuView mPipMenuView; private View mPipBackgroundView; + private boolean mMenuIsOpen; // User can actively move the PiP via the DPAD. private boolean mInMoveMode; // Used when only showing the move menu since we want to close the menu completely when @@ -77,13 +78,11 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis Matrix mMoveTransform = new Matrix(); public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState, - SystemWindows systemWindows, Handler mainHandler, - TvPipActionsProvider tvPipActionsProvider) { + SystemWindows systemWindows, Handler mainHandler) { mContext = context; mTvPipBoundsState = tvPipBoundsState; mSystemWindows = systemWindows; mMainHandler = mainHandler; - mTvPipActionsProvider = tvPipActionsProvider; // We need to "close" the menu the platform call for all the system dialogs to close (for // example, on the Home button press). @@ -112,6 +111,10 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis mDelegate = delegate; } + void setTvPipActionsProvider(TvPipActionsProvider tvPipActionsProvider) { + mTvPipActionsProvider = tvPipActionsProvider; + } + @Override public void attach(SurfaceControl leash) { if (mDelegate == null) { @@ -143,6 +146,11 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } private void attachPipMenuView() { + if (mTvPipActionsProvider == null) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Actions provider is not set", TAG); + return; + } mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this, mTvPipActionsProvider); setUpViewSurfaceZOrder(mPipMenuView, 1); addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE); @@ -186,12 +194,16 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis }); } - void showMovementMenuOnly() { + void showMovementMenu() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMovementMenuOnly()", TAG); setInMoveMode(true); - mCloseAfterExitMoveMenu = true; - showMenuInternal(); + if (mMenuIsOpen) { + mPipMenuView.showMoveMenu(mDelegate.getPipGravity()); + } else { + mCloseAfterExitMoveMenu = true; + showMenuInternal(); + } } @Override @@ -207,6 +219,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis return; } + mMenuIsOpen = true; grantPipMenuFocus(true); if (mInMoveMode) { mPipMenuView.showMoveMenu(mDelegate.getPipGravity()); @@ -237,6 +250,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (mPipMenuView == null) { return; } + + mMenuIsOpen = false; mPipMenuView.hideAllUserControls(); grantPipMenuFocus(false); mDelegate.onMenuClosed(); @@ -256,30 +271,20 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } } - @Override - public void onEnterMoveMode() { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onEnterMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode, - mCloseAfterExitMoveMenu); - setInMoveMode(true); - mPipMenuView.showMoveMenu(mDelegate.getPipGravity()); - } - @Override public boolean onExitMoveMode() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onExitMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode, - mCloseAfterExitMoveMenu); + "%s: onExitMoveMode - %b, close when exiting move menu: %b", + TAG, mInMoveMode, mCloseAfterExitMoveMenu); - if (mCloseAfterExitMoveMenu) { - setInMoveMode(false); - mCloseAfterExitMoveMenu = false; - closeMenu(); - return true; - } if (mInMoveMode) { setInMoveMode(false); - mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ true); + if (mCloseAfterExitMoveMenu) { + mCloseAfterExitMoveMenu = false; + closeMenu(); + } else { + mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ true); + } return true; } return false; @@ -496,21 +501,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } } - @Override - public void onCloseButtonClick() { - mDelegate.closePip(); - } - - @Override - public void onFullscreenButtonClick() { - mDelegate.movePipToFullscreen(); - } - - @Override - public void onToggleExpandedMode() { - mDelegate.togglePipExpansion(); - } - @Override public void onCloseEduText() { mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE); @@ -518,21 +508,15 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } interface Delegate { - void movePipToFullscreen(); - void movePip(int keycode); void onInMoveModeChanged(); int getPipGravity(); - void togglePipExpansion(); - void onMenuClosed(); void closeEduText(); - - void closePip(); } private void grantPipMenuFocus(boolean grantFocus) { 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 11ad290dfc2c2..56c602a1d4f3a 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 @@ -25,11 +25,6 @@ import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT; import static android.view.KeyEvent.KEYCODE_DPAD_UP; import static android.view.KeyEvent.KEYCODE_ENTER; -import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CLOSE; -import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM; -import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE; -import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE; -import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN; import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; import android.content.Context; @@ -516,26 +511,8 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L public void onClick(View v) { TvPipAction action = mActionList.get( mActionButtonsRecyclerView.getChildLayoutPosition(v)); - switch (action.getActionType()) { - case ACTION_FULLSCREEN: - mListener.onFullscreenButtonClick(); - return; - case ACTION_CLOSE: - case ACTION_CUSTOM_CLOSE: - mListener.onCloseButtonClick(); - return; - case ACTION_MOVE: - mListener.onEnterMoveMode(); - return; - case ACTION_EXPAND_COLLAPSE: - mListener.onToggleExpandedMode(); - return; - case ACTION_CUSTOM: - action.executePendingIntent(); - return; - default: - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: No action available", TAG); + if (action != null) { + action.executeAction(); } } } @@ -545,8 +522,6 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L void onBackPress(); - void onEnterMoveMode(); - /** * Called when a button for exiting move mode was pressed. * @@ -559,11 +534,5 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L * @return whether pip movement was handled. */ boolean onPipMovement(int keycode); - - void onCloseButtonClick(); - - void onFullscreenButtonClick(); - - void onToggleExpandedMode(); } } \ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java index 848d591f08a32..f22ee595e6c93 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java @@ -20,11 +20,9 @@ import android.annotation.NonNull; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; -import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; @@ -32,7 +30,6 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.media.session.MediaSession; import android.os.Bundle; -import android.os.Handler; import android.text.TextUtils; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; @@ -52,32 +49,17 @@ import java.util.List; * configuration changes and user initiated expanded PiP toggling. */ public class TvPipNotificationController implements TvPipActionsProvider.Listener { - private static final String TAG = "TvPipNotification"; + private static final String TAG = TvPipNotificationController.class.getSimpleName(); // Referenced in com.android.systemui.util.NotificationChannels. public static final String NOTIFICATION_CHANNEL = "TVPIP"; private static final String NOTIFICATION_TAG = "TvPip"; - private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF"; - - private static final String ACTION_SHOW_PIP_MENU = - "com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU"; - static final String ACTION_CLOSE_PIP = - "com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP"; - static final String ACTION_MOVE_PIP = - "com.android.wm.shell.pip.tv.notification.action.MOVE_PIP"; - static final String ACTION_TOGGLE_EXPANDED_PIP = - "com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP"; - static final String ACTION_TO_FULLSCREEN = - "com.android.wm.shell.pip.tv.notification.action.FULLSCREEN"; private final Context mContext; private final PackageManager mPackageManager; private final NotificationManager mNotificationManager; private final Notification.Builder mNotificationBuilder; - private final ActionBroadcastReceiver mActionBroadcastReceiver; - private final Handler mMainHandler; - private Delegate mDelegate; - private final TvPipActionsProvider mTvPipActionsProvider; + private TvPipActionsProvider mTvPipActionsProvider; private MediaSession.Token mMediaSessionToken; @@ -89,22 +71,17 @@ public class TvPipNotificationController implements TvPipActionsProvider.Listene private String mPipTitle; private String mPipSubtitle; - // Saving the actions so they don't have to be regenerated when e.g. the PiP title changes. + // Saving the actions, so they don't have to be regenerated when e.g. the PiP title changes. @NonNull private Notification.Action[] mPipActions; private Bitmap mActivityIcon; public TvPipNotificationController(Context context, PipMediaController pipMediaController, - PipParamsChangedForwarder pipParamsChangedForwarder, - TvPipActionsProvider tvPipActionsProvider, Handler mainHandler) { + PipParamsChangedForwarder pipParamsChangedForwarder) { mContext = context; mPackageManager = context.getPackageManager(); mNotificationManager = context.getSystemService(NotificationManager.class); - mMainHandler = mainHandler; - - mTvPipActionsProvider = tvPipActionsProvider; - mTvPipActionsProvider.addListener(this); mPipActions = new Notification.Action[0]; @@ -116,11 +93,10 @@ public class TvPipNotificationController implements TvPipActionsProvider.Listene .setOnlyAlertOnce(true) .setSmallIcon(R.drawable.pip_icon) .setAllowSystemGeneratedContextualActions(false) - .setContentIntent(createPendingIntent(context, ACTION_TO_FULLSCREEN)); + .setContentIntent( + createPendingIntent(context, TvPipController.ACTION_TO_FULLSCREEN)); // TvExtender and DeleteIntent set later since they might change. - mActionBroadcastReceiver = new ActionBroadcastReceiver(); - pipMediaController.addTokenListener(this::onMediaSessionTokenChanged); pipParamsChangedForwarder.addListener( @@ -141,35 +117,30 @@ public class TvPipNotificationController implements TvPipActionsProvider.Listene onConfigurationChanged(); } + /** + * Call before showing any notification. + */ + void setTvPipActionsProvider(@NonNull TvPipActionsProvider tvPipActionsProvider) { + mTvPipActionsProvider = tvPipActionsProvider; + mTvPipActionsProvider.addListener(this); + } + void onConfigurationChanged() { mDefaultTitle = mContext.getResources().getString(R.string.pip_notification_unknown_title); updateNotificationContent(); } - void setDelegate(Delegate delegate) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: setDelegate(), delegate=%s", - TAG, delegate); - - if (mDelegate != null) { - throw new IllegalStateException( - "The delegate has already been set and should not change."); - } - if (delegate == null) { - throw new IllegalArgumentException("The delegate must not be null."); - } - mDelegate = delegate; - } - void show(String packageName) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: show %s", TAG, packageName); - if (mDelegate == null) { - throw new IllegalStateException("Delegate is not set."); + if (mTvPipActionsProvider == null) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Missing TvPipActionsProvider", TAG); + return; } mIsNotificationShown = true; mPackageName = packageName; mActivityIcon = getActivityIcon(); - mActionBroadcastReceiver.register(); updateNotificationContent(); } @@ -179,8 +150,6 @@ public class TvPipNotificationController implements TvPipActionsProvider.Listene mIsNotificationShown = false; mPackageName = null; - mActionBroadcastReceiver.unregister(); - mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP); } @@ -213,7 +182,8 @@ public class TvPipNotificationController implements TvPipActionsProvider.Listene mNotificationBuilder.setDeleteIntent(closeIntent); // TvExtender not recognized if not set last. mNotificationBuilder.extend(new Notification.TvExtender() - .setContentIntent(createPendingIntent(mContext, ACTION_SHOW_PIP_MENU)) + .setContentIntent( + createPendingIntent(mContext, TvPipController.ACTION_SHOW_PIP_MENU)) .setDeleteIntent(closeIntent)); mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP, @@ -283,62 +253,4 @@ public class TvPipNotificationController implements TvPipActionsProvider.Listene updateNotificationContent(); } - private class ActionBroadcastReceiver extends BroadcastReceiver { - final IntentFilter mIntentFilter; - { - mIntentFilter = new IntentFilter(); - mIntentFilter.addAction(ACTION_CLOSE_PIP); - mIntentFilter.addAction(ACTION_SHOW_PIP_MENU); - mIntentFilter.addAction(ACTION_MOVE_PIP); - mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP); - mIntentFilter.addAction(ACTION_TO_FULLSCREEN); - } - boolean mRegistered = false; - - void register() { - if (mRegistered) return; - - mContext.registerReceiverForAllUsers(this, mIntentFilter, SYSTEMUI_PERMISSION, - mMainHandler); - mRegistered = true; - } - - void unregister() { - if (!mRegistered) return; - - mContext.unregisterReceiver(this); - mRegistered = false; - } - - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: on(Broadcast)Receive(), action=%s", TAG, action); - - if (ACTION_SHOW_PIP_MENU.equals(action)) { - mDelegate.showPictureInPictureMenu(); - } else if (ACTION_CLOSE_PIP.equals(action)) { - mDelegate.closePip(); - } else if (ACTION_MOVE_PIP.equals(action)) { - mDelegate.enterPipMovementMenu(); - } else if (ACTION_TOGGLE_EXPANDED_PIP.equals(action)) { - mDelegate.togglePipExpansion(); - } else if (ACTION_TO_FULLSCREEN.equals(action)) { - mDelegate.movePipToFullscreen(); - } - } - } - - interface Delegate { - void showPictureInPictureMenu(); - - void closePip(); - - void enterPipMovementMenu(); - - void togglePipExpansion(); - - void movePipToFullscreen(); - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java index 8bf5d2a724c24..93b6a908e3f48 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java @@ -44,8 +44,9 @@ public class TvPipSystemAction extends TvPipAction { private final PendingIntent mBroadcastIntent; TvPipSystemAction(@ActionType int actionType, @StringRes int title, @DrawableRes int icon, - String broadcastAction, @NonNull Context context) { - super(actionType); + String broadcastAction, @NonNull Context context, + SystemActionsHandler systemActionsHandler) { + super(actionType, systemActionsHandler); update(title, icon); mBroadcastIntent = TvPipNotificationController.createPendingIntent(context, broadcastAction); @@ -63,7 +64,7 @@ public class TvPipSystemAction extends TvPipAction { } PendingIntent getPendingIntent() { - return null; + return mBroadcastIntent; } @Override diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java index 67d1ad76aea58..91040e9548456 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java @@ -63,6 +63,8 @@ public class TvPipActionProviderTest extends ShellTestCase { @Mock private TvPipActionsProvider.Listener mMockListener; @Mock + private TvPipAction.SystemActionsHandler mMockSystemActionsHandler; + @Mock private Icon mMockIcon; @Mock private PendingIntent mMockPendingIntent; @@ -94,7 +96,8 @@ public class TvPipActionProviderTest extends ShellTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mActionsProvider = new TvPipActionsProvider(mContext, mMockPipMediaController); + mActionsProvider = new TvPipActionsProvider(mContext, mMockPipMediaController, + mMockSystemActionsHandler); } @Test