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
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 8022e9b..d64d92b 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.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 @@
             PipTaskOrganizer pipTaskOrganizer,
             TvPipMenuController tvPipMenuController,
             PipMediaController pipMediaController,
+            TvPipActionsProvider tvPipActionsProvider,
             PipTransitionController pipTransitionController,
             TvPipNotificationController tvPipNotificationController,
             TaskStackListenerImpl taskStackListener,
@@ -95,6 +97,7 @@
                         pipTransitionController,
                         tvPipMenuController,
                         pipMediaController,
+                        tvPipActionsProvider,
                         tvPipNotificationController,
                         taskStackListener,
                         pipParamsChangedForwarder,
@@ -157,10 +160,10 @@
             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 @@
     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 @@
             @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 9f8f95c..1364229 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 @@
 
 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 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 0000000..2214ad1
--- /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<Listener> mListeners = new ArrayList<>();
+
+    private final List<TvPipAction> mActionsList;
+    private final TvPipSystemAction mDefaultCloseAction;
+    private final TvPipSystemAction mExpandCollapseAction;
+
+    private final List<RemoteAction> mMediaActions = new ArrayList<>();
+    private final List<RemoteAction> 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<RemoteAction> 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<RemoteAction> 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<RemoteAction> customActions) {
+        List<RemoteAction> 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<TvPipAction> 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<TvPipAction> 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 3e8de45..f5691c5 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 @@
     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 @@
             PipTransitionController pipTransitionController,
             TvPipMenuController tvPipMenuController,
             PipMediaController pipMediaController,
+            TvPipActionsProvider tvPipActionsProvider,
             TvPipNotificationController pipNotificationController,
             TaskStackListenerImpl taskStackListener,
             PipParamsChangedForwarder pipParamsChangedForwarder,
@@ -159,6 +161,7 @@
                 pipTransitionController,
                 tvPipMenuController,
                 pipMediaController,
+                tvPipActionsProvider,
                 pipNotificationController,
                 taskStackListener,
                 pipParamsChangedForwarder,
@@ -179,6 +182,7 @@
             PipTransitionController pipTransitionController,
             TvPipMenuController tvPipMenuController,
             PipMediaController pipMediaController,
+            TvPipActionsProvider tvPipActionsProvider,
             TvPipNotificationController pipNotificationController,
             TaskStackListenerImpl taskStackListener,
             PipParamsChangedForwarder pipParamsChangedForwarder,
@@ -198,6 +202,7 @@
         mTvPipBoundsController.setListener(this);
 
         mPipMediaController = pipMediaController;
+        mTvPipActionsProvider = tvPipActionsProvider;
 
         mPipNotificationController = pipNotificationController;
         mPipNotificationController.setDelegate(this);
@@ -241,7 +246,7 @@
                 "%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState));
 
         loadConfigurations();
-        mPipNotificationController.onConfigurationChanged(mContext);
+        mPipNotificationController.onConfigurationChanged();
         mTvPipBoundsAlgorithm.onConfigurationChanged(mContext);
     }
 
@@ -310,7 +315,6 @@
         }
         mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding);
         mTvPipBoundsState.setTvPipExpanded(expanding);
-        mPipNotificationController.updateExpansionState();
 
         updatePinnedStackBounds();
     }
@@ -373,7 +377,7 @@
     @Override
     public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) {
         mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds,
-                animationDuration, rect -> mTvPipMenuController.updateExpansionState());
+                animationDuration, null);
         mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds);
     }
 
@@ -454,6 +458,11 @@
 
     @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 @@
                 "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState));
         mTvPipMenuController.onPipTransitionFinished(
                 PipAnimationController.isInPipDirection(direction));
+        mTvPipActionsProvider.onPipExpansionToggled(mTvPipBoundsState.isTvPipExpanded());
     }
 
     @Override
@@ -477,6 +487,12 @@
                 "%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 @@
                 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 @@
                         "%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 5c0b1b3..4bd240a 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 @@
         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 e80602e..bcb995a 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 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 @@
     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 @@
     // exiting the move menu instead of showing the regular button menu.
     private boolean mCloseAfterExitMoveMenu;
 
-    private final List<RemoteAction> mMediaActions = new ArrayList<>();
-    private final List<RemoteAction> mAppActions = new ArrayList<>();
-    private RemoteAction mCloseAction;
-
     private SyncRtSurfaceTransactionApplier mApplier;
     private SyncRtSurfaceTransactionApplier mBackgroundApplier;
     RectF mTmpSourceRectF = new RectF();
@@ -83,12 +77,13 @@
     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 @@
         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 @@
     }
 
     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 @@
         // 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 @@
         if (mPipMenuView == null) {
             return;
         }
-        maybeUpdateMenuViewActions();
-        updateExpansionState();
 
         grantPipMenuFocus(true);
         if (mInMoveMode) {
@@ -236,11 +226,6 @@
         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 @@
 
     @Override
     public void setAppActions(List<RemoteAction> actions, RemoteAction closeAction) {
-        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: setAppActions()", TAG);
-        updateAdditionalActionsList(mAppActions, actions, closeAction);
-    }
-
-    private void onMediaActionsChanged(List<RemoteAction> actions) {
-        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: onMediaActionsChanged()", TAG);
-
-        // Hide disabled actions.
-        List<RemoteAction> enabledActions = new ArrayList<>();
-        for (RemoteAction remoteAction : actions) {
-            if (remoteAction.isEnabled()) {
-                enabledActions.add(remoteAction);
-            }
-        }
-        updateAdditionalActionsList(mMediaActions, enabledActions, mCloseAction);
-    }
-
-    private void updateAdditionalActionsList(List<RemoteAction> destination,
-            @Nullable List<RemoteAction> 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 d8dc022..11ad290 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_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.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<TvPipAction> 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 @@
     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 @@
         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 @@
         }
     }
 
-    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 @@
             mEduTextDrawer.init();
         }
 
-        // Update buttons.
-        setIsExpanded(isTvPipExpanded);
-
         if (mSwitchingOrientation) {
             mActionButtonsRecyclerView.animate()
                     .alpha(1)
@@ -294,26 +269,6 @@
         }
     }
 
-    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 @@
         mEduTextDrawer.closeIfNeeded();
 
         if (exitingMoveMode) {
-            scrollAndRefocusButton(getFirstIndexOfAction(ACTION_MOVE),
+            scrollAndRefocusButton(mTvPipActionsProvider.getFirstIndexOfAction(ACTION_MOVE),
                     /* alwaysScroll= */ false);
         } else {
             scrollAndRefocusButton(0, /* alwaysScroll= */ true);
@@ -369,18 +324,6 @@
         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 @@
                 });
     }
 
-    void setAdditionalActions(List<RemoteAction> 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<TvPipAction> 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 e3308f0..848d591 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.PipUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
-import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -55,7 +51,7 @@
  * <p>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 @@
 
     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 @@
     private final ActionBroadcastReceiver mActionBroadcastReceiver;
     private final Handler mMainHandler;
     private Delegate mDelegate;
-    private final TvPipBoundsState mTvPipBoundsState;
-
-    private String mDefaultTitle;
-
-    private final List<RemoteAction> mCustomActions = new ArrayList<>();
-    private final List<RemoteAction> mMediaActions = new ArrayList<>();
-    private RemoteAction mCustomCloseAction;
+    private final TvPipActionsProvider mTvPipActionsProvider;
 
     private MediaSession.Token mMediaSessionToken;
 
@@ -95,19 +85,28 @@
     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,34 +116,16 @@
                 .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<RemoteAction> actions,
-                            RemoteAction closeAction) {
-                        mCustomActions.clear();
-                        mCustomActions.addAll(actions);
-                        mCustomCloseAction = closeAction;
-                        updateNotificationContent();
-                    }
-
-                    @Override
                     public void onTitleChanged(String title) {
                         mPipTitle = title;
                         updateNotificationContent();
@@ -157,7 +138,12 @@
                     }
                 });
 
-        onConfigurationChanged(context);
+        onConfigurationChanged();
+    }
+
+    void onConfigurationChanged() {
+        mDefaultTitle = mContext.getResources().getString(R.string.pip_notification_unknown_title);
+        updateNotificationContent();
     }
 
     void setDelegate(Delegate delegate) {
@@ -171,7 +157,6 @@
         if (delegate == null) {
             throw new IllegalArgumentException("The delegate must not be null.");
         }
-
         mDelegate = delegate;
     }
 
@@ -199,146 +184,38 @@
         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<RemoteAction> 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<Notification.Action> actions = new ArrayList<>();
-
-        // 1. Fullscreen
-        actions.add(getFullscreenAction());
-        // 2. Close
-        actions.add(getCloseAction());
-        // 3. App actions
-        final List<RemoteAction> 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 @@
         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<TvPipAction> 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 @@
             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 @@
                 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 83a26c3..8bf5d2a 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 @@
     @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 @@
         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 0000000..67d1ad7
--- /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<RemoteAction> createRemoteActions(int numberOfActions) {
+        List<RemoteAction> actions = new ArrayList<>();
+        for (int i = 0; i < numberOfActions; i++) {
+            actions.add(createRemoteAction(i));
+        }
+        return actions;
+    }
+
+    private boolean checkActionsMatch(List<TvPipAction> 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<RemoteAction> 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<RemoteAction> 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<RemoteAction> 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<RemoteAction> 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}));
+    }
+
+}
+