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})); + } + +} +