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
This commit is contained in:
Jacqueline Bronger
2022-11-08 17:29:48 +01:00
parent bc2095c842
commit 5e56e0c021
10 changed files with 699 additions and 357 deletions

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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<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 @@ 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<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

View File

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

View File

@@ -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;
* <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 @@ 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<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 @@ 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<RemoteAction> 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<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 @@ 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<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 @@ 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();
}
}

View File

@@ -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();
}
}

View File

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