From 5e56e0c0211f0e896aaf3428ca4849e1940c4080 Mon Sep 17 00:00:00 2001 From: Jacqueline Bronger Date: Tue, 8 Nov 2022 17:29:48 +0100 Subject: [PATCH] Handle all TvPipActions within one component. Introduces a TvPipActionsProvider that keeps the list of all the pip actions that should be displayed. It gets updates about the PiP from the TvPipController and sends changes to the actions to its listeners (TvPipMenuView and TvPipNotificationController). Bug: 258653494 Test: atest TvPipActionProviderTest Test: manual - check PiP menu content is populated Change-Id: Ibf892d7d3fb3efc1182faabca68b9ef772c9c606 --- .../android/wm/shell/dagger/TvPipModule.java | 20 +- .../android/wm/shell/pip/tv/TvPipAction.java | 4 + .../wm/shell/pip/tv/TvPipActionsProvider.java | 232 +++++++++++++ .../wm/shell/pip/tv/TvPipController.java | 26 +- .../wm/shell/pip/tv/TvPipCustomAction.java | 23 ++ .../wm/shell/pip/tv/TvPipMenuController.java | 77 +---- .../wm/shell/pip/tv/TvPipMenuView.java | 137 ++------ .../pip/tv/TvPipNotificationController.java | 205 +++--------- .../wm/shell/pip/tv/TvPipSystemAction.java | 26 +- .../shell/pip/tv/TvPipActionProviderTest.java | 306 ++++++++++++++++++ 10 files changed, 699 insertions(+), 357 deletions(-) create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java create mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java index 8022e9b1cd810..d64d92b36335b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java @@ -39,6 +39,7 @@ import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipTransitionState; import com.android.wm.shell.pip.PipUiEventLogger; +import com.android.wm.shell.pip.tv.TvPipActionsProvider; import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm; import com.android.wm.shell.pip.tv.TvPipBoundsController; import com.android.wm.shell.pip.tv.TvPipBoundsState; @@ -75,6 +76,7 @@ public abstract class TvPipModule { PipTaskOrganizer pipTaskOrganizer, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, + TvPipActionsProvider tvPipActionsProvider, PipTransitionController pipTransitionController, TvPipNotificationController tvPipNotificationController, TaskStackListenerImpl taskStackListener, @@ -95,6 +97,7 @@ public abstract class TvPipModule { pipTransitionController, tvPipMenuController, pipMediaController, + tvPipActionsProvider, tvPipNotificationController, taskStackListener, pipParamsChangedForwarder, @@ -157,10 +160,10 @@ public abstract class TvPipModule { Context context, TvPipBoundsState tvPipBoundsState, SystemWindows systemWindows, - PipMediaController pipMediaController, + TvPipActionsProvider tvPipActionsProvider, @ShellMainThread Handler mainHandler) { - return new TvPipMenuController(context, tvPipBoundsState, systemWindows, pipMediaController, - mainHandler); + return new TvPipMenuController(context, tvPipBoundsState, systemWindows, mainHandler, + tvPipActionsProvider); } // Handler needed for registerReceiverForAllUsers() @@ -169,10 +172,10 @@ public abstract class TvPipModule { static TvPipNotificationController provideTvPipNotificationController(Context context, PipMediaController pipMediaController, PipParamsChangedForwarder pipParamsChangedForwarder, - TvPipBoundsState tvPipBoundsState, + TvPipActionsProvider tvPipActionsProvider, @ShellMainThread Handler mainHandler) { return new TvPipNotificationController(context, pipMediaController, - pipParamsChangedForwarder, tvPipBoundsState, mainHandler); + pipParamsChangedForwarder, tvPipActionsProvider, mainHandler); } @WMSingleton @@ -224,4 +227,11 @@ public abstract class TvPipModule { @ShellMainThread ShellExecutor mainExecutor) { return new PipAppOpsListener(context, pipTaskOrganizer::removePip, mainExecutor); } + + @WMSingleton + @Provides + static TvPipActionsProvider provideTvPipActionsProvider(Context context, + PipMediaController pipMediaController) { + return new TvPipActionsProvider(context, pipMediaController); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java index 9f8f95c93fec2..1364229082025 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java @@ -18,7 +18,9 @@ package com.android.wm.shell.pip.tv; import android.annotation.IntDef; import android.annotation.NonNull; +import android.app.Notification; import android.app.PendingIntent; +import android.content.Context; import android.os.Handler; import com.android.internal.protolog.common.ProtoLog; @@ -81,4 +83,6 @@ abstract class TvPipAction { } } + abstract Notification.Action toNotificationAction(Context context); + } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java new file mode 100644 index 0000000000000..2214ad166220b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip.tv; + +import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; +import static com.android.wm.shell.pip.tv.TvPipNotificationController.ACTION_CLOSE_PIP; +import static com.android.wm.shell.pip.tv.TvPipNotificationController.ACTION_MOVE_PIP; +import static com.android.wm.shell.pip.tv.TvPipNotificationController.ACTION_TOGGLE_EXPANDED_PIP; +import static com.android.wm.shell.pip.tv.TvPipNotificationController.ACTION_TO_FULLSCREEN; + +import android.annotation.NonNull; +import android.app.RemoteAction; +import android.content.Context; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipUtils; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * Creates the system TvPipActions (fullscreen, close, move, expand/collapse), and handles all the + * changes to the actions, including the custom app actions and media actions. Other components can + * listen to those changes. + */ +public class TvPipActionsProvider { + private static final String TAG = TvPipActionsProvider.class.getSimpleName(); + + private static final int CLOSE_ACTION_INDEX = 1; + private static final int FIRST_CUSTOM_ACTION_INDEX = 2; + + private final List mListeners = new ArrayList<>(); + + private final List mActionsList; + private final TvPipSystemAction mDefaultCloseAction; + private final TvPipSystemAction mExpandCollapseAction; + + private final List mMediaActions = new ArrayList<>(); + private final List mAppActions = new ArrayList<>(); + + public TvPipActionsProvider(Context context, PipMediaController pipMediaController) { + + mActionsList = new ArrayList<>(); + mActionsList.add(new TvPipSystemAction(ACTION_FULLSCREEN, R.string.pip_fullscreen, + R.drawable.pip_ic_fullscreen_white, ACTION_TO_FULLSCREEN, context)); + + mDefaultCloseAction = new TvPipSystemAction(ACTION_CLOSE, R.string.pip_close, + R.drawable.pip_ic_close_white, ACTION_CLOSE_PIP, context); + mActionsList.add(mDefaultCloseAction); + + mActionsList.add(new TvPipSystemAction(ACTION_MOVE, R.string.pip_move, + R.drawable.pip_ic_move_white, ACTION_MOVE_PIP, context)); + + mExpandCollapseAction = new TvPipSystemAction(ACTION_EXPAND_COLLAPSE, R.string.pip_collapse, + R.drawable.pip_ic_collapse, ACTION_TOGGLE_EXPANDED_PIP, context); + mActionsList.add(mExpandCollapseAction); + + pipMediaController.addActionListener(this::onMediaActionsChanged); + } + + private void notifyActionsChanged(int added, int changed, int startIndex) { + for (Listener listener : mListeners) { + listener.onActionsChanged(added, changed, startIndex); + } + } + + @VisibleForTesting(visibility = PACKAGE) + public void setAppActions(@NonNull List appActions, RemoteAction closeAction) { + // Update close action. + mActionsList.set(CLOSE_ACTION_INDEX, + closeAction == null ? mDefaultCloseAction + : new TvPipCustomAction(ACTION_CUSTOM_CLOSE, closeAction)); + notifyActionsChanged(/* added= */ 0, /* updated= */ 1, CLOSE_ACTION_INDEX); + + // Replace custom actions with new ones. + mAppActions.clear(); + for (RemoteAction action : appActions) { + if (action != null && !PipUtils.remoteActionsMatch(action, closeAction)) { + // Only show actions that aren't duplicates of the custom close action. + mAppActions.add(action); + } + } + + updateCustomActions(mAppActions); + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public void onMediaActionsChanged(List actions) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onMediaActionsChanged()", TAG); + + mMediaActions.clear(); + // Don't show disabled actions. + for (RemoteAction remoteAction : actions) { + if (remoteAction.isEnabled()) { + mMediaActions.add(remoteAction); + } + } + + updateCustomActions(mMediaActions); + } + + private void updateCustomActions(@NonNull List customActions) { + List newCustomActions = customActions; + if (newCustomActions == mMediaActions && !mAppActions.isEmpty()) { + // Don't show the media actions while there are app actions. + return; + } else if (newCustomActions == mAppActions && mAppActions.isEmpty()) { + // If all the app actions were removed, show the media actions. + newCustomActions = mMediaActions; + } + + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: replaceCustomActions, count: %d", TAG, newCustomActions.size()); + int oldCustomActionsCount = 0; + for (TvPipAction action : mActionsList) { + if (action.getActionType() == ACTION_CUSTOM) { + oldCustomActionsCount++; + } + } + mActionsList.removeIf(tvPipAction -> tvPipAction.getActionType() == ACTION_CUSTOM); + + List actions = new ArrayList<>(); + for (RemoteAction action : newCustomActions) { + actions.add(new TvPipCustomAction(ACTION_CUSTOM, action)); + } + mActionsList.addAll(FIRST_CUSTOM_ACTION_INDEX, actions); + + int added = newCustomActions.size() - oldCustomActionsCount; + int changed = Math.min(newCustomActions.size(), oldCustomActionsCount); + notifyActionsChanged(added, changed, FIRST_CUSTOM_ACTION_INDEX); + } + + @VisibleForTesting(visibility = PACKAGE) + public void updateExpansionEnabled(boolean enabled) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: updateExpansionState, enabled: %b", TAG, enabled); + int actionIndex = mActionsList.indexOf(mExpandCollapseAction); + boolean actionInList = actionIndex != -1; + if (enabled && !actionInList) { + mActionsList.add(mExpandCollapseAction); + actionIndex = mActionsList.size() - 1; + } else if (!enabled && actionInList) { + mActionsList.remove(actionIndex); + } else { + return; + } + notifyActionsChanged(/* added= */ enabled ? 1 : -1, /* updated= */ 0, actionIndex); + } + + @VisibleForTesting(visibility = PACKAGE) + public void onPipExpansionToggled(boolean expanded) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipExpansionToggled, expanded: %b", TAG, expanded); + + mExpandCollapseAction.update( + expanded ? R.string.pip_collapse : R.string.pip_expand, + expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand); + + notifyActionsChanged(/* added= */ 0, /* updated= */ 1, + mActionsList.indexOf(mExpandCollapseAction)); + } + + List getActionsList() { + return mActionsList; + } + + @NonNull + TvPipAction getCloseAction() { + return mActionsList.get(CLOSE_ACTION_INDEX); + } + + void addListener(Listener listener) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + } + } + + /** + * Returns the index of the first action of the given action type or -1 if none can be found. + */ + int getFirstIndexOfAction(@TvPipAction.ActionType int actionType) { + for (int i = 0; i < mActionsList.size(); i++) { + if (mActionsList.get(i).getActionType() == actionType) { + return i; + } + } + return -1; + } + + /** + * Allow components to listen to updates to the actions list, including where they happen so + * that changes can be animated. + */ + interface Listener { + /** + * Notifies the listener how many actions were added/removed or updated. + * + * @param added can be positive (number of actions added), negative (number of actions + * removed) or zero (the number of actions stayed the same). + * @param updated the number of actions that might have been updated and need to be + * refreshed. + * @param startIndex The index of the first updated action. The added/removed actions start + * at (startIndex + updated). + */ + void onActionsChanged(int added, int updated, int startIndex); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java index 3e8de454bcff6..f5691c5b112c2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -107,6 +107,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private final PipAppOpsListener mAppOpsListener; private final PipTaskOrganizer mPipTaskOrganizer; private final PipMediaController mPipMediaController; + private final TvPipActionsProvider mTvPipActionsProvider; private final TvPipNotificationController mPipNotificationController; private final TvPipMenuController mTvPipMenuController; private final PipTransitionController mPipTransitionController; @@ -141,6 +142,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, + TvPipActionsProvider tvPipActionsProvider, TvPipNotificationController pipNotificationController, TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, @@ -159,6 +161,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal pipTransitionController, tvPipMenuController, pipMediaController, + tvPipActionsProvider, pipNotificationController, taskStackListener, pipParamsChangedForwarder, @@ -179,6 +182,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, + TvPipActionsProvider tvPipActionsProvider, TvPipNotificationController pipNotificationController, TaskStackListenerImpl taskStackListener, PipParamsChangedForwarder pipParamsChangedForwarder, @@ -198,6 +202,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mTvPipBoundsController.setListener(this); mPipMediaController = pipMediaController; + mTvPipActionsProvider = tvPipActionsProvider; mPipNotificationController = pipNotificationController; mPipNotificationController.setDelegate(this); @@ -241,7 +246,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState)); loadConfigurations(); - mPipNotificationController.onConfigurationChanged(mContext); + mPipNotificationController.onConfigurationChanged(); mTvPipBoundsAlgorithm.onConfigurationChanged(mContext); } @@ -310,7 +315,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding); mTvPipBoundsState.setTvPipExpanded(expanding); - mPipNotificationController.updateExpansionState(); updatePinnedStackBounds(); } @@ -373,7 +377,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) { mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds, - animationDuration, rect -> mTvPipMenuController.updateExpansionState()); + animationDuration, null); mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds); } @@ -454,6 +458,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void onPipTransitionStarted(int direction, Rect currentPipBounds) { + final boolean enterPipTransition = PipAnimationController.isInPipDirection(direction); + if (enterPipTransition && mState == STATE_NO_PIP) { + // Set the initial ability to expand the PiP when entering PiP. + updateExpansionState(); + } ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransition_Started(), state=%s, direction=%d", TAG, stateToName(mState), direction); @@ -465,6 +474,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState)); mTvPipMenuController.onPipTransitionFinished( PipAnimationController.isInPipDirection(direction)); + mTvPipActionsProvider.onPipExpansionToggled(mTvPipBoundsState.isTvPipExpanded()); } @Override @@ -477,6 +487,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: onPipTransition_Finished(), state=%s, direction=%d", TAG, stateToName(mState), direction); mTvPipMenuController.onPipTransitionFinished(enterPipTransition); + mTvPipActionsProvider.onPipExpansionToggled(mTvPipBoundsState.isTvPipExpanded()); + } + + private void updateExpansionState() { + mTvPipActionsProvider.updateExpansionEnabled(mTvPipBoundsState.isTvExpandedPipSupported() + && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0); } private void setState(@State int state) { @@ -534,7 +550,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onActionsChanged()", TAG); - mTvPipMenuController.setAppActions(actions, closeAction); + mTvPipActionsProvider.setAppActions(actions, closeAction); mCloseAction = closeAction; } @@ -555,7 +571,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: onExpandedAspectRatioChanged: %f", TAG, ratio); mTvPipBoundsState.setDesiredTvExpandedAspectRatio(ratio, false); - mTvPipMenuController.updateExpansionState(); + updateExpansionState(); // 1) PiP is expanded and only aspect ratio changed, but wasn't disabled // --> update bounds, but don't toggle diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java index 5c0b1b3aa383d..4bd240afb5638 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java @@ -16,9 +16,15 @@ package com.android.wm.shell.pip.tv; +import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE; +import static android.app.Notification.Action.SEMANTIC_ACTION_NONE; + import android.annotation.NonNull; +import android.app.Notification; import android.app.PendingIntent; import android.app.RemoteAction; +import android.content.Context; +import android.os.Bundle; import android.os.Handler; import com.android.wm.shell.common.TvWindowMenuActionButton; @@ -56,4 +62,21 @@ public class TvPipCustomAction extends TvPipAction { return mRemoteAction.getActionIntent(); } + @Override + Notification.Action toNotificationAction(Context context) { + Notification.Action.Builder builder = new Notification.Action.Builder( + mRemoteAction.getIcon(), + mRemoteAction.getTitle(), + mRemoteAction.getActionIntent()); + Bundle extras = new Bundle(); + extras.putCharSequence(Notification.EXTRA_PICTURE_CONTENT_DESCRIPTION, + mRemoteAction.getContentDescription()); + builder.addExtras(extras); + + builder.setSemanticAction(isCloseAction() + ? SEMANTIC_ACTION_DELETE : SEMANTIC_ACTION_NONE); + builder.setContextual(true); + return builder.build(); + } + } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index e80602ea0cd92..bcb995ae8a090 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -41,13 +41,10 @@ import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.SystemWindows; -import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.ArrayList; import java.util.List; -import java.util.Objects; /** * Manages the visibility of the PiP Menu as user interacts with PiP. @@ -60,6 +57,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private final SystemWindows mSystemWindows; private final TvPipBoundsState mTvPipBoundsState; private final Handler mMainHandler; + private final TvPipActionsProvider mTvPipActionsProvider; private Delegate mDelegate; private SurfaceControl mLeash; @@ -72,10 +70,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis // exiting the move menu instead of showing the regular button menu. private boolean mCloseAfterExitMoveMenu; - private final List mMediaActions = new ArrayList<>(); - private final List mAppActions = new ArrayList<>(); - private RemoteAction mCloseAction; - private SyncRtSurfaceTransactionApplier mApplier; private SyncRtSurfaceTransactionApplier mBackgroundApplier; RectF mTmpSourceRectF = new RectF(); @@ -83,12 +77,13 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis Matrix mMoveTransform = new Matrix(); public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState, - SystemWindows systemWindows, PipMediaController pipMediaController, - Handler mainHandler) { + SystemWindows systemWindows, Handler mainHandler, + TvPipActionsProvider tvPipActionsProvider) { mContext = context; mTvPipBoundsState = tvPipBoundsState; mSystemWindows = systemWindows; mMainHandler = mainHandler; + mTvPipActionsProvider = tvPipActionsProvider; // We need to "close" the menu the platform call for all the system dialogs to close (for // example, on the Home button press). @@ -101,9 +96,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis context.registerReceiverForAllUsers(closeSystemDialogsBroadcastReceiver, new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), null /* permission */, mainHandler, Context.RECEIVER_EXPORTED); - - pipMediaController.addActionListener(this::onMediaActionsChanged); - } void setDelegate(Delegate delegate) { @@ -151,10 +143,9 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } private void attachPipMenuView() { - mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this); + mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this, mTvPipActionsProvider); setUpViewSurfaceZOrder(mPipMenuView, 1); addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE); - maybeUpdateMenuViewActions(); } private void attachPipBackgroundView() { @@ -189,8 +180,9 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis // and the menu view has been fully remeasured and relaid out, we add a small delay here by // posting on the handler. mMainHandler.post(() -> { - mPipMenuView.onPipTransitionFinished( - enterTransition, mTvPipBoundsState.isTvPipExpanded()); + if (mPipMenuView != null) { + mPipMenuView.onPipTransitionFinished(enterTransition); + } }); } @@ -214,8 +206,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (mPipMenuView == null) { return; } - maybeUpdateMenuViewActions(); - updateExpansionState(); grantPipMenuFocus(true); if (mInMoveMode) { @@ -236,11 +226,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis mPipMenuView.showMovementHints(gravity); } - void updateExpansionState() { - mPipMenuView.setExpandedModeEnabled(mTvPipBoundsState.isTvExpandedPipSupported() - && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0); - } - private Rect calculateMenuSurfaceBounds(Rect pipBounds) { return mPipMenuView.getPipMenuContainerBounds(pipBounds); } @@ -319,51 +304,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis @Override public void setAppActions(List actions, RemoteAction closeAction) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setAppActions()", TAG); - updateAdditionalActionsList(mAppActions, actions, closeAction); - } - - private void onMediaActionsChanged(List actions) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onMediaActionsChanged()", TAG); - - // Hide disabled actions. - List enabledActions = new ArrayList<>(); - for (RemoteAction remoteAction : actions) { - if (remoteAction.isEnabled()) { - enabledActions.add(remoteAction); - } - } - updateAdditionalActionsList(mMediaActions, enabledActions, mCloseAction); - } - - private void updateAdditionalActionsList(List destination, - @Nullable List source, RemoteAction closeAction) { - final int number = source != null ? source.size() : 0; - if (number == 0 && destination.isEmpty() && Objects.equals(closeAction, mCloseAction)) { - // Nothing changed. - return; - } - - mCloseAction = closeAction; - - destination.clear(); - if (number > 0) { - destination.addAll(source); - } - maybeUpdateMenuViewActions(); - } - - private void maybeUpdateMenuViewActions() { - if (mPipMenuView == null) { - return; - } - if (!mAppActions.isEmpty()) { - mPipMenuView.setAdditionalActions(mAppActions, mCloseAction); - } else { - mPipMenuView.setAdditionalActions(mMediaActions, mCloseAction); - } + // NOOP - handled via the TvPipActionsProvider } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index d8dc022ad59c2..11ad290dfc2c2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -32,7 +32,6 @@ import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE; import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN; import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; -import android.app.RemoteAction; import android.content.Context; import android.graphics.Rect; import android.os.Handler; @@ -54,25 +53,18 @@ import com.android.wm.shell.common.TvWindowMenuActionButton; import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.ArrayList; import java.util.List; /** - * A View that represents Pip Menu on TV. It's responsible for displaying 3 ever-present Pip Menu - * actions: Fullscreen, Move and Close, but could also display "additional" actions, that may be set - * via a {@link #setAdditionalActions(List, RemoteAction, Handler)} call. + * A View that represents Pip Menu on TV. It's responsible for displaying the Pip menu actions from + * the TvPipActionsProvider as well as the buttons for manually moving the PiP. */ -public class TvPipMenuView extends FrameLayout { +public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.Listener { private static final String TAG = "TvPipMenuView"; - private static final int CLOSE_ACTION_INDEX = 1; - private static final int FIRST_CUSTOM_ACTION_INDEX = 2; - private final TvPipMenuView.Listener mListener; - private final List mActionsList; - private final TvPipSystemAction mDefaultCloseAction; - private final TvPipSystemAction mExpandCollapseAction; + private final TvPipActionsProvider mTvPipActionsProvider; private final RecyclerView mActionButtonsRecyclerView; private final LinearLayoutManager mButtonLayoutManager; @@ -108,7 +100,7 @@ public class TvPipMenuView extends FrameLayout { private final Handler mMainHandler; public TvPipMenuView(@NonNull Context context, @NonNull Handler mainHandler, - @NonNull Listener listener) { + @NonNull Listener listener, TvPipActionsProvider tvPipActionsProvider) { super(context, null, 0, 0); inflate(context, R.layout.tv_pip_menu, this); @@ -121,26 +113,12 @@ public class TvPipMenuView extends FrameLayout { mActionButtonsRecyclerView.setLayoutManager(mButtonLayoutManager); mActionButtonsRecyclerView.setPreserveFocusAfterLayout(true); - mDefaultCloseAction = - new TvPipSystemAction(ACTION_CLOSE, R.string.pip_close, - R.drawable.pip_ic_close_white); - mExpandCollapseAction = - new TvPipSystemAction(ACTION_EXPAND_COLLAPSE, R.string.pip_collapse, - R.drawable.pip_ic_collapse); - - mActionsList = new ArrayList<>(); - mActionsList.add( - new TvPipSystemAction(ACTION_FULLSCREEN, R.string.pip_fullscreen, - R.drawable.pip_ic_fullscreen_white)); - mActionsList.add(mDefaultCloseAction); - mActionsList.add( - new TvPipSystemAction(ACTION_MOVE, R.string.pip_move, - R.drawable.pip_ic_move_white)); - mActionsList.add(mExpandCollapseAction); - - mRecyclerViewAdapter = new RecyclerViewAdapter(mActionsList); + mTvPipActionsProvider = tvPipActionsProvider; + mRecyclerViewAdapter = new RecyclerViewAdapter(tvPipActionsProvider.getActionsList()); mActionButtonsRecyclerView.setAdapter(mRecyclerViewAdapter); + tvPipActionsProvider.addListener(this); + mMenuFrameView = findViewById(R.id.tv_pip_menu_frame); mPipFrameView = findViewById(R.id.tv_pip_border); mPipView = findViewById(R.id.tv_pip); @@ -217,7 +195,7 @@ public class TvPipMenuView extends FrameLayout { } } - void onPipTransitionFinished(boolean enterTransition, boolean isTvPipExpanded) { + void onPipTransitionFinished(boolean enterTransition) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransitionFinished()", TAG); @@ -232,9 +210,6 @@ public class TvPipMenuView extends FrameLayout { mEduTextDrawer.init(); } - // Update buttons. - setIsExpanded(isTvPipExpanded); - if (mSwitchingOrientation) { mActionButtonsRecyclerView.animate() .alpha(1) @@ -294,26 +269,6 @@ public class TvPipMenuView extends FrameLayout { } } - void setExpandedModeEnabled(boolean enabled) { - int actionIndex = mActionsList.indexOf(mExpandCollapseAction); - boolean actionInList = actionIndex != -1; - if (enabled && !actionInList) { - mActionsList.add(mExpandCollapseAction); - mRecyclerViewAdapter.notifyItemInserted(mActionsList.size() - 1); - } else if (!enabled && actionInList) { - mActionsList.remove(actionIndex); - mRecyclerViewAdapter.notifyItemRemoved(actionIndex); - } - } - - void setIsExpanded(boolean expanded) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setIsExpanded, expanded: %b", TAG, expanded); - mExpandCollapseAction.update(expanded ? R.string.pip_collapse : R.string.pip_expand, - expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand); - mRecyclerViewAdapter.notifyItemChanged(mActionsList.indexOf(mExpandCollapseAction)); - } - /** * @param gravity for the arrow hints */ @@ -339,7 +294,7 @@ public class TvPipMenuView extends FrameLayout { mEduTextDrawer.closeIfNeeded(); if (exitingMoveMode) { - scrollAndRefocusButton(getFirstIndexOfAction(ACTION_MOVE), + scrollAndRefocusButton(mTvPipActionsProvider.getFirstIndexOfAction(ACTION_MOVE), /* alwaysScroll= */ false); } else { scrollAndRefocusButton(0, /* alwaysScroll= */ true); @@ -369,18 +324,6 @@ public class TvPipMenuView extends FrameLayout { return itemToFocus != null; } - /** - * Returns the position of the first action of the given action type or -1 if none can be found. - */ - private int getFirstIndexOfAction(@TvPipAction.ActionType int actionType) { - for (int i = 0; i < mActionsList.size(); i++) { - if (mActionsList.get(i).getActionType() == actionType) { - return i; - } - } - return -1; - } - void hideAllUserControls() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: hideAllUserControls()", TAG); @@ -418,57 +361,13 @@ public class TvPipMenuView extends FrameLayout { }); } - void setAdditionalActions(List actions, RemoteAction closeAction) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setAdditionalActions(), %d actions", TAG, actions.size()); - - int oldCustomActionCount = 0; - for (TvPipAction action : mActionsList) { - if (action.getActionType() == ACTION_CUSTOM) { - oldCustomActionCount++; - } - } - - // Update close action. - mActionsList.set(CLOSE_ACTION_INDEX, - closeAction == null ? mDefaultCloseAction - : new TvPipCustomAction(ACTION_CUSTOM_CLOSE, closeAction)); - mRecyclerViewAdapter.notifyItemChanged(CLOSE_ACTION_INDEX); - - // Replace custom actions with new ones. - mActionsList.removeIf(tvPipAction -> tvPipAction.getActionType() == ACTION_CUSTOM); - List customActions = new ArrayList<>(actions.size()); - int newCustomActionCount = 0; - for (RemoteAction action : actions) { - if (action == null || PipUtils.remoteActionsMatch(action, closeAction)) { - // Don't show an action if it is the same as the custom close action - continue; - } - customActions.add(new TvPipCustomAction(ACTION_CUSTOM, action)); - newCustomActionCount++; - } - mActionsList.addAll(FIRST_CUSTOM_ACTION_INDEX, customActions); - - mRecyclerViewAdapter.notifyItemRangeChanged( - FIRST_CUSTOM_ACTION_INDEX, Math.min(oldCustomActionCount, newCustomActionCount)); - - if (newCustomActionCount > oldCustomActionCount) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setAdditionalActions(), %d inserted starting at %d", - TAG, newCustomActionCount - oldCustomActionCount, - FIRST_CUSTOM_ACTION_INDEX + oldCustomActionCount); - mRecyclerViewAdapter.notifyItemRangeInserted( - FIRST_CUSTOM_ACTION_INDEX + oldCustomActionCount, - newCustomActionCount - oldCustomActionCount); - - } else if (oldCustomActionCount > newCustomActionCount) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setAdditionalActions(), %d removed starting at %d", - TAG, oldCustomActionCount - newCustomActionCount, - FIRST_CUSTOM_ACTION_INDEX + newCustomActionCount); - mRecyclerViewAdapter.notifyItemRangeRemoved( - FIRST_CUSTOM_ACTION_INDEX + newCustomActionCount, - oldCustomActionCount - newCustomActionCount); + @Override + public void onActionsChanged(int added, int updated, int startIndex) { + mRecyclerViewAdapter.notifyItemRangeChanged(startIndex, updated); + if (added > 0) { + mRecyclerViewAdapter.notifyItemRangeInserted(startIndex + updated, added); + } else if (added < 0) { + mRecyclerViewAdapter.notifyItemRangeRemoved(startIndex + updated, -added); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java index e3308f0763a04..848d591f08a32 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java @@ -16,13 +16,10 @@ package com.android.wm.shell.pip.tv; -import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE; -import static android.app.Notification.Action.SEMANTIC_ACTION_NONE; - +import android.annotation.NonNull; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; -import android.app.RemoteAction; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -47,7 +44,6 @@ import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.ArrayList; import java.util.List; /** @@ -55,7 +51,7 @@ import java.util.List; *

Once it's created, it will manage the PiP notification UI by itself except for handling * configuration changes and user initiated expanded PiP toggling. */ -public class TvPipNotificationController { +public class TvPipNotificationController implements TvPipActionsProvider.Listener { private static final String TAG = "TvPipNotification"; // Referenced in com.android.systemui.util.NotificationChannels. @@ -65,13 +61,13 @@ public class TvPipNotificationController { private static final String ACTION_SHOW_PIP_MENU = "com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU"; - private static final String ACTION_CLOSE_PIP = + static final String ACTION_CLOSE_PIP = "com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP"; - private static final String ACTION_MOVE_PIP = + static final String ACTION_MOVE_PIP = "com.android.wm.shell.pip.tv.notification.action.MOVE_PIP"; - private static final String ACTION_TOGGLE_EXPANDED_PIP = + static final String ACTION_TOGGLE_EXPANDED_PIP = "com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP"; - private static final String ACTION_FULLSCREEN = + static final String ACTION_TO_FULLSCREEN = "com.android.wm.shell.pip.tv.notification.action.FULLSCREEN"; private final Context mContext; @@ -81,13 +77,7 @@ public class TvPipNotificationController { private final ActionBroadcastReceiver mActionBroadcastReceiver; private final Handler mMainHandler; private Delegate mDelegate; - private final TvPipBoundsState mTvPipBoundsState; - - private String mDefaultTitle; - - private final List mCustomActions = new ArrayList<>(); - private final List mMediaActions = new ArrayList<>(); - private RemoteAction mCustomCloseAction; + private final TvPipActionsProvider mTvPipActionsProvider; private MediaSession.Token mMediaSessionToken; @@ -95,19 +85,28 @@ public class TvPipNotificationController { private String mPackageName; private boolean mIsNotificationShown; + private String mDefaultTitle; private String mPipTitle; private String mPipSubtitle; + // Saving the actions so they don't have to be regenerated when e.g. the PiP title changes. + @NonNull + private Notification.Action[] mPipActions; + private Bitmap mActivityIcon; public TvPipNotificationController(Context context, PipMediaController pipMediaController, - PipParamsChangedForwarder pipParamsChangedForwarder, TvPipBoundsState tvPipBoundsState, - Handler mainHandler) { + PipParamsChangedForwarder pipParamsChangedForwarder, + TvPipActionsProvider tvPipActionsProvider, Handler mainHandler) { mContext = context; mPackageManager = context.getPackageManager(); mNotificationManager = context.getSystemService(NotificationManager.class); mMainHandler = mainHandler; - mTvPipBoundsState = tvPipBoundsState; + + mTvPipActionsProvider = tvPipActionsProvider; + mTvPipActionsProvider.addListener(this); + + mPipActions = new Notification.Action[0]; mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL) .setLocalOnly(true) @@ -117,33 +116,15 @@ public class TvPipNotificationController { .setOnlyAlertOnce(true) .setSmallIcon(R.drawable.pip_icon) .setAllowSystemGeneratedContextualActions(false) - .setContentIntent(createPendingIntent(context, ACTION_FULLSCREEN)) - .setDeleteIntent(getCloseAction().actionIntent) - .extend(new Notification.TvExtender() - .setContentIntent(createPendingIntent(context, ACTION_SHOW_PIP_MENU)) - .setDeleteIntent(createPendingIntent(context, ACTION_CLOSE_PIP))); + .setContentIntent(createPendingIntent(context, ACTION_TO_FULLSCREEN)); + // TvExtender and DeleteIntent set later since they might change. mActionBroadcastReceiver = new ActionBroadcastReceiver(); - pipMediaController.addActionListener(this::onMediaActionsChanged); pipMediaController.addTokenListener(this::onMediaSessionTokenChanged); pipParamsChangedForwarder.addListener( new PipParamsChangedForwarder.PipParamsChangedCallback() { - @Override - public void onExpandedAspectRatioChanged(float ratio) { - updateExpansionState(); - } - - @Override - public void onActionsChanged(List actions, - RemoteAction closeAction) { - mCustomActions.clear(); - mCustomActions.addAll(actions); - mCustomCloseAction = closeAction; - updateNotificationContent(); - } - @Override public void onTitleChanged(String title) { mPipTitle = title; @@ -157,7 +138,12 @@ public class TvPipNotificationController { } }); - onConfigurationChanged(context); + onConfigurationChanged(); + } + + void onConfigurationChanged() { + mDefaultTitle = mContext.getResources().getString(R.string.pip_notification_unknown_title); + updateNotificationContent(); } void setDelegate(Delegate delegate) { @@ -171,7 +157,6 @@ public class TvPipNotificationController { if (delegate == null) { throw new IllegalArgumentException("The delegate must not be null."); } - mDelegate = delegate; } @@ -199,146 +184,38 @@ public class TvPipNotificationController { mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP); } - private Notification.Action getToggleAction(boolean expanded) { - if (expanded) { - return createSystemAction(R.drawable.pip_ic_collapse, - R.string.pip_collapse, ACTION_TOGGLE_EXPANDED_PIP); - } else { - return createSystemAction(R.drawable.pip_ic_expand, R.string.pip_expand, - ACTION_TOGGLE_EXPANDED_PIP); - } - } - - private Notification.Action createSystemAction(int iconRes, int titleRes, String action) { - Notification.Action.Builder builder = new Notification.Action.Builder( - Icon.createWithResource(mContext, iconRes), - mContext.getString(titleRes), - createPendingIntent(mContext, action)); - builder.setContextual(true); - return builder.build(); - } - - private void onMediaActionsChanged(List actions) { - mMediaActions.clear(); - mMediaActions.addAll(actions); - if (mCustomActions.isEmpty()) { - updateNotificationContent(); - } - } - private void onMediaSessionTokenChanged(MediaSession.Token token) { mMediaSessionToken = token; updateNotificationContent(); } - private Notification.Action remoteToNotificationAction(RemoteAction action) { - return remoteToNotificationAction(action, SEMANTIC_ACTION_NONE); - } - - private Notification.Action remoteToNotificationAction(RemoteAction action, - int semanticAction) { - Notification.Action.Builder builder = new Notification.Action.Builder(action.getIcon(), - action.getTitle(), - action.getActionIntent()); - if (action.getContentDescription() != null) { - Bundle extras = new Bundle(); - extras.putCharSequence(Notification.EXTRA_PICTURE_CONTENT_DESCRIPTION, - action.getContentDescription()); - builder.addExtras(extras); - } - builder.setSemanticAction(semanticAction); - builder.setContextual(true); - return builder.build(); - } - - private Notification.Action[] getNotificationActions() { - final List actions = new ArrayList<>(); - - // 1. Fullscreen - actions.add(getFullscreenAction()); - // 2. Close - actions.add(getCloseAction()); - // 3. App actions - final List appActions = - mCustomActions.isEmpty() ? mMediaActions : mCustomActions; - for (RemoteAction appAction : appActions) { - if (PipUtils.remoteActionsMatch(mCustomCloseAction, appAction) - || !appAction.isEnabled()) { - continue; - } - actions.add(remoteToNotificationAction(appAction)); - } - // 4. Move - actions.add(getMoveAction()); - // 5. Toggle expansion (if expanded PiP enabled) - if (mTvPipBoundsState.getDesiredTvExpandedAspectRatio() > 0 - && mTvPipBoundsState.isTvExpandedPipSupported()) { - actions.add(getToggleAction(mTvPipBoundsState.isTvPipExpanded())); - } - return actions.toArray(new Notification.Action[0]); - } - - private Notification.Action getCloseAction() { - if (mCustomCloseAction == null) { - return createSystemAction(R.drawable.pip_ic_close_white, R.string.pip_close, - ACTION_CLOSE_PIP); - } else { - return remoteToNotificationAction(mCustomCloseAction, SEMANTIC_ACTION_DELETE); - } - } - - private Notification.Action getFullscreenAction() { - return createSystemAction(R.drawable.pip_ic_fullscreen_white, - R.string.pip_fullscreen, ACTION_FULLSCREEN); - } - - private Notification.Action getMoveAction() { - return createSystemAction(R.drawable.pip_ic_move_white, R.string.pip_move, - ACTION_MOVE_PIP); - } - - /** - * Called by {@link TvPipController} when the configuration is changed. - */ - void onConfigurationChanged(Context context) { - mDefaultTitle = context.getResources().getString(R.string.pip_notification_unknown_title); - updateNotificationContent(); - } - - void updateExpansionState() { - updateNotificationContent(); - } - private void updateNotificationContent() { if (mPackageManager == null || !mIsNotificationShown) { return; } - Notification.Action[] actions = getNotificationActions(); ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: update(), title: %s, subtitle: %s, mediaSessionToken: %s, #actions: %s", TAG, - getNotificationTitle(), mPipSubtitle, mMediaSessionToken, actions.length); - for (Notification.Action action : actions) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: action: %s", TAG, - action.toString()); - } - + getNotificationTitle(), mPipSubtitle, mMediaSessionToken, mPipActions.length); mNotificationBuilder .setWhen(System.currentTimeMillis()) .setContentTitle(getNotificationTitle()) .setContentText(mPipSubtitle) .setSubText(getApplicationLabel(mPackageName)) - .setActions(actions); + .setActions(mPipActions); setPipIcon(); Bundle extras = new Bundle(); extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, mMediaSessionToken); mNotificationBuilder.setExtras(extras); + PendingIntent closeIntent = mTvPipActionsProvider.getCloseAction().getPendingIntent(); + mNotificationBuilder.setDeleteIntent(closeIntent); // TvExtender not recognized if not set last. mNotificationBuilder.extend(new Notification.TvExtender() .setContentIntent(createPendingIntent(mContext, ACTION_SHOW_PIP_MENU)) - .setDeleteIntent(createPendingIntent(mContext, ACTION_CLOSE_PIP))); + .setDeleteIntent(closeIntent)); + mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP, mNotificationBuilder.build()); } @@ -390,12 +267,22 @@ public class TvPipNotificationController { return ImageUtils.buildScaledBitmap(drawable, width, height, /* allowUpscaling */ true); } - private static PendingIntent createPendingIntent(Context context, String action) { + static PendingIntent createPendingIntent(Context context, String action) { return PendingIntent.getBroadcast(context, 0, new Intent(action).setPackage(context.getPackageName()), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } + @Override + public void onActionsChanged(int added, int updated, int startIndex) { + List actions = mTvPipActionsProvider.getActionsList(); + mPipActions = new Notification.Action[actions.size()]; + for (int i = 0; i < mPipActions.length; i++) { + mPipActions[i] = actions.get(i).toNotificationAction(mContext); + } + updateNotificationContent(); + } + private class ActionBroadcastReceiver extends BroadcastReceiver { final IntentFilter mIntentFilter; { @@ -404,7 +291,7 @@ public class TvPipNotificationController { mIntentFilter.addAction(ACTION_SHOW_PIP_MENU); mIntentFilter.addAction(ACTION_MOVE_PIP); mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP); - mIntentFilter.addAction(ACTION_FULLSCREEN); + mIntentFilter.addAction(ACTION_TO_FULLSCREEN); } boolean mRegistered = false; @@ -437,7 +324,7 @@ public class TvPipNotificationController { mDelegate.enterPipMovementMenu(); } else if (ACTION_TOGGLE_EXPANDED_PIP.equals(action)) { mDelegate.togglePipExpansion(); - } else if (ACTION_FULLSCREEN.equals(action)) { + } else if (ACTION_TO_FULLSCREEN.equals(action)) { mDelegate.movePipToFullscreen(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java index 83a26c351bcfe..8bf5d2a724c24 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java @@ -16,10 +16,16 @@ package com.android.wm.shell.pip.tv; +import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE; +import static android.app.Notification.Action.SEMANTIC_ACTION_NONE; + import android.annotation.DrawableRes; import android.annotation.NonNull; import android.annotation.StringRes; +import android.app.Notification; import android.app.PendingIntent; +import android.content.Context; +import android.graphics.drawable.Icon; import android.os.Handler; import com.android.wm.shell.common.TvWindowMenuActionButton; @@ -35,9 +41,14 @@ public class TvPipSystemAction extends TvPipAction { @DrawableRes private int mIconResource; - TvPipSystemAction(@ActionType int actionType, @StringRes int title, @DrawableRes int icon) { + private final PendingIntent mBroadcastIntent; + + TvPipSystemAction(@ActionType int actionType, @StringRes int title, @DrawableRes int icon, + String broadcastAction, @NonNull Context context) { super(actionType); update(title, icon); + mBroadcastIntent = TvPipNotificationController.createPendingIntent(context, + broadcastAction); } void update(@StringRes int title, @DrawableRes int icon) { @@ -55,4 +66,17 @@ public class TvPipSystemAction extends TvPipAction { return null; } + @Override + Notification.Action toNotificationAction(Context context) { + Notification.Action.Builder builder = new Notification.Action.Builder( + Icon.createWithResource(context, mIconResource), + context.getString(mTitleResource), + mBroadcastIntent); + + builder.setSemanticAction(isCloseAction() + ? SEMANTIC_ACTION_DELETE : SEMANTIC_ACTION_NONE); + builder.setContextual(true); + return builder.build(); + } + } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java new file mode 100644 index 0000000000000..67d1ad76aea58 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip.tv; + +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.graphics.drawable.Icon; +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.Log; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.pip.PipMediaController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +/** + * Unit tests for {@link TvPipActionsProvider} + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class TvPipActionProviderTest extends ShellTestCase { + private static final String TAG = TvPipActionProviderTest.class.getSimpleName(); + private TvPipActionsProvider mActionsProvider; + + @Mock + private PipMediaController mMockPipMediaController; + @Mock + private TvPipActionsProvider.Listener mMockListener; + @Mock + private Icon mMockIcon; + @Mock + private PendingIntent mMockPendingIntent; + + private RemoteAction createRemoteAction(int identifier) { + return new RemoteAction(mMockIcon, "" + identifier, "" + identifier, mMockPendingIntent); + } + + private List createRemoteActions(int numberOfActions) { + List actions = new ArrayList<>(); + for (int i = 0; i < numberOfActions; i++) { + actions.add(createRemoteAction(i)); + } + return actions; + } + + private boolean checkActionsMatch(List actions, int[] actionTypes) { + for (int i = 0; i < actions.size(); i++) { + int type = actions.get(i).getActionType(); + if (type != actionTypes[i]) { + Log.e(TAG, "Action at index " + i + ": found " + type + + ", expected " + actionTypes[i]); + return false; + } + } + return true; + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mActionsProvider = new TvPipActionsProvider(mContext, mMockPipMediaController); + } + + @Test + public void defaultSystemActions_regularPip() { + mActionsProvider.updateExpansionEnabled(false); + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE})); + } + + @Test + public void defaultSystemActions_expandedPip() { + mActionsProvider.updateExpansionEnabled(true); + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + } + + @Test + public void expandedPip_enableExpansion_enable() { + // PiP has expanded PiP disabled. + mActionsProvider.updateExpansionEnabled(false); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.updateExpansionEnabled(true); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + verify(mMockListener).onActionsChanged(/* added= */ 1, /* updated= */ 0, /* index= */ 3); + } + + @Test + public void expandedPip_enableExpansion_disable() { + mActionsProvider.updateExpansionEnabled(true); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.updateExpansionEnabled(false); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ -1, /* updated= */ 0, /* index= */ 3); + } + + @Test + public void expandedPip_enableExpansion_AlreadyEnabled() { + mActionsProvider.updateExpansionEnabled(true); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.updateExpansionEnabled(true); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + } + + @Test + public void expandedPip_toggleExpansion() { + // PiP has expanded PiP enabled, but is in a collapsed state + mActionsProvider.updateExpansionEnabled(true); + mActionsProvider.onPipExpansionToggled(/* expanded= */ false); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.onPipExpansionToggled(/* expanded= */ true); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + verify(mMockListener).onActionsChanged(0, 1, 3); + } + + @Test + public void customActions_added() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.addListener(mMockListener); + + mActionsProvider.setAppActions(createRemoteActions(2), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 2, /* updated= */ 0, /* index= */ 2); + } + + @Test + public void customActions_replacedMore() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.setAppActions(createRemoteActions(2), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(3), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_CUSTOM, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 1, /* updated= */ 2, /* index= */ 2); + } + + @Test + public void customActions_replacedLess() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.setAppActions(createRemoteActions(2), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(0), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ -2, /* updated= */ 0, /* index= */ 2); + } + + @Test + public void customCloseAdded() { + mActionsProvider.updateExpansionEnabled(false); + + List customActions = new ArrayList<>(); + mActionsProvider.setAppActions(customActions, null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(customActions, createRemoteAction(0)); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1); + } + + @Test + public void customClose_matchesOtherCustomAction() { + mActionsProvider.updateExpansionEnabled(false); + + List customActions = createRemoteActions(2); + RemoteAction customClose = createRemoteAction(/* id= */ 10); + customActions.add(customClose); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(customActions, customClose); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1); + verify(mMockListener).onActionsChanged(/* added= */ 2, /* updated= */ 0, /* index= */ 2); + } + + @Test + public void mediaActions_added_whileCustomActionsExist() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.setAppActions(createRemoteActions(2), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.onMediaActionsChanged(createRemoteActions(3)); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener, times(0)).onActionsChanged(anyInt(), anyInt(), anyInt()); + } + + @Test + public void customActions_removed_whileMediaActionsExist() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.onMediaActionsChanged(createRemoteActions(2)); + mActionsProvider.setAppActions(createRemoteActions(3), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(0), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ -1, /* updated= */ 2, /* index= */ 2); + } + + @Test + public void customCloseOnly_mediaActionsShowing() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.onMediaActionsChanged(createRemoteActions(2)); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(0), createRemoteAction(5)); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1); + } + + @Test + public void customActions_showDisabledActions() { + mActionsProvider.updateExpansionEnabled(false); + + List customActions = createRemoteActions(2); + customActions.get(0).setEnabled(false); + mActionsProvider.setAppActions(customActions, null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + } + + @Test + public void mediaActions_hideDisabledActions() { + mActionsProvider.updateExpansionEnabled(false); + + List customActions = createRemoteActions(2); + customActions.get(0).setEnabled(false); + mActionsProvider.onMediaActionsChanged(customActions); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_MOVE})); + } + +} +