actionList) {
+ this.mActionList = actionList;
+ }
+
+ @NonNull
+ @Override
+ public ButtonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new ButtonViewHolder(new TvWindowMenuActionButton(mContext));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ButtonViewHolder holder, int position) {
+ TvPipAction action = mActionList.get(position);
+ action.populateButton(holder.mButton, mMainHandler);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mActionList.size();
+ }
+
+ private class ButtonViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
+ TvWindowMenuActionButton mButton;
+
+ ButtonViewHolder(@NonNull View itemView) {
+ super(itemView);
+ mButton = (TvWindowMenuActionButton) itemView;
+ mButton.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ TvPipAction action = mActionList.get(
+ mActionButtonsRecyclerView.getChildLayoutPosition(v));
+ if (action != null) {
+ action.executeAction();
+ }
+ }
+ }
+ }
+
interface Listener extends TvPipMenuEduTextDrawer.Listener {
void onBackPress();
- void onEnterMoveMode();
-
/**
* Called when a button for exiting move mode was pressed.
*
@@ -723,11 +534,5 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
* @return whether pip movement was handled.
*/
boolean onPipMovement(int keycode);
-
- void onCloseButtonClick();
-
- void onFullscreenButtonClick();
-
- void onToggleExpandedMode();
}
-}
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
index e3308f0763a04..f22ee595e6c93 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
@@ -16,18 +16,13 @@
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;
import android.content.Intent;
-import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
@@ -35,7 +30,6 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.media.session.MediaSession;
import android.os.Bundle;
-import android.os.Handler;
import android.text.TextUtils;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
@@ -47,7 +41,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,39 +48,18 @@ 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 {
- private static final String TAG = "TvPipNotification";
+public class TvPipNotificationController implements TvPipActionsProvider.Listener {
+ private static final String TAG = TvPipNotificationController.class.getSimpleName();
// Referenced in com.android.systemui.util.NotificationChannels.
public static final String NOTIFICATION_CHANNEL = "TVPIP";
private static final String NOTIFICATION_TAG = "TvPip";
- private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";
-
- private static final String ACTION_SHOW_PIP_MENU =
- "com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU";
- private static final String ACTION_CLOSE_PIP =
- "com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP";
- private static final String ACTION_MOVE_PIP =
- "com.android.wm.shell.pip.tv.notification.action.MOVE_PIP";
- private static final String ACTION_TOGGLE_EXPANDED_PIP =
- "com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP";
- private static final String ACTION_FULLSCREEN =
- "com.android.wm.shell.pip.tv.notification.action.FULLSCREEN";
private final Context mContext;
private final PackageManager mPackageManager;
private final NotificationManager mNotificationManager;
private final Notification.Builder mNotificationBuilder;
- private final ActionBroadcastReceiver mActionBroadcastReceiver;
- private final Handler mMainHandler;
- private Delegate mDelegate;
- private final TvPipBoundsState mTvPipBoundsState;
-
- private String mDefaultTitle;
-
- private final List mCustomActions = new ArrayList<>();
- private final List mMediaActions = new ArrayList<>();
- private RemoteAction mCustomCloseAction;
+ private TvPipActionsProvider mTvPipActionsProvider;
private MediaSession.Token mMediaSessionToken;
@@ -95,19 +67,23 @@ 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) {
mContext = context;
mPackageManager = context.getPackageManager();
mNotificationManager = context.getSystemService(NotificationManager.class);
- mMainHandler = mainHandler;
- mTvPipBoundsState = tvPipBoundsState;
+
+ mPipActions = new Notification.Action[0];
mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL)
.setLocalOnly(true)
@@ -117,33 +93,14 @@ 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, TvPipController.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,34 +114,33 @@ public class TvPipNotificationController {
}
});
- onConfigurationChanged(context);
+ onConfigurationChanged();
}
- void setDelegate(Delegate delegate) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: setDelegate(), delegate=%s",
- TAG, delegate);
+ /**
+ * Call before showing any notification.
+ */
+ void setTvPipActionsProvider(@NonNull TvPipActionsProvider tvPipActionsProvider) {
+ mTvPipActionsProvider = tvPipActionsProvider;
+ mTvPipActionsProvider.addListener(this);
+ }
- if (mDelegate != null) {
- throw new IllegalStateException(
- "The delegate has already been set and should not change.");
- }
- if (delegate == null) {
- throw new IllegalArgumentException("The delegate must not be null.");
- }
-
- mDelegate = delegate;
+ void onConfigurationChanged() {
+ mDefaultTitle = mContext.getResources().getString(R.string.pip_notification_unknown_title);
+ updateNotificationContent();
}
void show(String packageName) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: show %s", TAG, packageName);
- if (mDelegate == null) {
- throw new IllegalStateException("Delegate is not set.");
+ if (mTvPipActionsProvider == null) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Missing TvPipActionsProvider", TAG);
+ return;
}
mIsNotificationShown = true;
mPackageName = packageName;
mActivityIcon = getActivityIcon();
- mActionBroadcastReceiver.register();
updateNotificationContent();
}
@@ -194,151 +150,42 @@ public class TvPipNotificationController {
mIsNotificationShown = false;
mPackageName = null;
- mActionBroadcastReceiver.unregister();
-
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)));
+ .setContentIntent(
+ createPendingIntent(mContext, TvPipController.ACTION_SHOW_PIP_MENU))
+ .setDeleteIntent(closeIntent));
+
mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP,
mNotificationBuilder.build());
}
@@ -390,68 +237,20 @@ 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);
}
- private class ActionBroadcastReceiver extends BroadcastReceiver {
- final IntentFilter mIntentFilter;
- {
- mIntentFilter = new IntentFilter();
- mIntentFilter.addAction(ACTION_CLOSE_PIP);
- mIntentFilter.addAction(ACTION_SHOW_PIP_MENU);
- mIntentFilter.addAction(ACTION_MOVE_PIP);
- mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP);
- mIntentFilter.addAction(ACTION_FULLSCREEN);
- }
- boolean mRegistered = false;
-
- void register() {
- if (mRegistered) return;
-
- mContext.registerReceiverForAllUsers(this, mIntentFilter, SYSTEMUI_PERMISSION,
- mMainHandler);
- mRegistered = true;
- }
-
- void unregister() {
- if (!mRegistered) return;
-
- mContext.unregisterReceiver(this);
- mRegistered = false;
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- final String action = intent.getAction();
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: on(Broadcast)Receive(), action=%s", TAG, action);
-
- if (ACTION_SHOW_PIP_MENU.equals(action)) {
- mDelegate.showPictureInPictureMenu();
- } else if (ACTION_CLOSE_PIP.equals(action)) {
- mDelegate.closePip();
- } else if (ACTION_MOVE_PIP.equals(action)) {
- mDelegate.enterPipMovementMenu();
- } else if (ACTION_TOGGLE_EXPANDED_PIP.equals(action)) {
- mDelegate.togglePipExpansion();
- } else if (ACTION_FULLSCREEN.equals(action)) {
- mDelegate.movePipToFullscreen();
- }
+ @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();
}
- interface Delegate {
- void showPictureInPictureMenu();
-
- void closePip();
-
- void enterPipMovementMenu();
-
- void togglePipExpansion();
-
- void movePipToFullscreen();
- }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java
new file mode 100644
index 0000000000000..93b6a908e3f48
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java
@@ -0,0 +1,83 @@
+/*
+ * 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 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;
+
+/**
+ * A TvPipAction for actions that the system provides, i.e. fullscreen, default close, move,
+ * expand/collapse.
+ */
+public class TvPipSystemAction extends TvPipAction {
+
+ @StringRes
+ private int mTitleResource;
+ @DrawableRes
+ private int mIconResource;
+
+ private final PendingIntent mBroadcastIntent;
+
+ TvPipSystemAction(@ActionType int actionType, @StringRes int title, @DrawableRes int icon,
+ String broadcastAction, @NonNull Context context,
+ SystemActionsHandler systemActionsHandler) {
+ super(actionType, systemActionsHandler);
+ update(title, icon);
+ mBroadcastIntent = TvPipNotificationController.createPendingIntent(context,
+ broadcastAction);
+ }
+
+ void update(@StringRes int title, @DrawableRes int icon) {
+ mTitleResource = title;
+ mIconResource = icon;
+ }
+
+ void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler) {
+ button.setTextAndDescription(mTitleResource);
+ button.setImageResource(mIconResource);
+ button.setEnabled(true);
+ }
+
+ PendingIntent getPendingIntent() {
+ return mBroadcastIntent;
+ }
+
+ @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..91040e9548456
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java
@@ -0,0 +1,309 @@
+/*
+ * 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 TvPipAction.SystemActionsHandler mMockSystemActionsHandler;
+ @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,
+ mMockSystemActionsHandler);
+ }
+
+ @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}));
+ }
+
+}
+