Implement custom PiP close action on TV.

The custom close action will replace the system close action and
the button in the menu will be colored red to be able to tell it
apart.
If one of the custom actions matches the close actions it will be
removed to avoid duplicates resulting from the custom close action.

If the app doesn't close the PiP after five seconds the system will
continue to close the PiP as normal.

Bug: 218487423
Test: manual
Change-Id: Ic3c520c50ac0e3d2f0e73cf2f71d47a4f548f861
This commit is contained in:
Jacqueline Bronger
2022-02-08 14:11:30 +01:00
parent d8a9414be0
commit aa3a7e6273
17 changed files with 179 additions and 41 deletions

View File

@@ -47,7 +47,7 @@ oneway interface IPinnedTaskListener {
* Called when the set of actions for the current PiP activity changes, or when the listener
* is first registered to allow the listener to synchronize its state with the controller.
*/
void onActionsChanged(in ParceledListSlice<RemoteAction> actions);
void onActionsChanged(in ParceledListSlice<RemoteAction> actions, in RemoteAction closeAction);
/**
* Called by the window manager to notify the listener that Activity (was or is in pinned mode)

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/tv_pip_menu_icon_unfocused" />
</selector>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true"
android:color="@color/tv_pip_menu_close_icon_bg_focused" />
<item android:color="@color/tv_pip_menu_close_icon_bg_unfocused" />
</selector>

View File

@@ -39,4 +39,8 @@
<!-- Duration (in milliseconds) the PiP stays stashed before automatically unstashing. -->
<integer name="config_pipStashDuration">5000</integer>
<!-- Time (duration in milliseconds) that the shell waits for an app to close the PiP by itself
if a custom action is present before closing it. -->
<integer name="config_pipForceCloseDelay">5000</integer>
</resources>

View File

@@ -16,8 +16,10 @@
-->
<resources>
<color name="tv_pip_menu_icon_focused">#0E0E0F</color>
<color name="tv_pip_menu_icon_unfocused">#E8EAED</color>
<color name="tv_pip_menu_icon_unfocused">#F8F9FA</color>
<color name="tv_pip_menu_icon_disabled">#80868B</color>
<color name="tv_pip_menu_close_icon_bg_focused">#D93025</color>
<color name="tv_pip_menu_close_icon_bg_unfocused">#D69F261F</color>
<color name="tv_pip_menu_icon_bg_focused">#E8EAED</color>
<color name="tv_pip_menu_icon_bg_unfocused">#990E0E0F</color>
<color name="tv_pip_menu_focus_border">#E8EAED</color>

View File

@@ -72,9 +72,10 @@ public class PinnedStackListenerForwarder {
}
}
private void onActionsChanged(ParceledListSlice<RemoteAction> actions) {
private void onActionsChanged(ParceledListSlice<RemoteAction> actions,
RemoteAction closeAction) {
for (PinnedTaskListener listener : mListeners) {
listener.onActionsChanged(actions);
listener.onActionsChanged(actions, closeAction);
}
}
@@ -113,9 +114,10 @@ public class PinnedStackListenerForwarder {
}
@Override
public void onActionsChanged(ParceledListSlice<RemoteAction> actions) {
public void onActionsChanged(ParceledListSlice<RemoteAction> actions,
RemoteAction closeAction) {
mMainExecutor.execute(() -> {
PinnedStackListenerForwarder.this.onActionsChanged(actions);
PinnedStackListenerForwarder.this.onActionsChanged(actions, closeAction);
});
}
@@ -152,7 +154,8 @@ public class PinnedStackListenerForwarder {
public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {}
public void onActionsChanged(ParceledListSlice<RemoteAction> actions) {}
public void onActionsChanged(ParceledListSlice<RemoteAction> actions,
RemoteAction closeAction) {}
public void onActivityHidden(ComponentName componentName) {}

View File

@@ -66,7 +66,7 @@ public interface PipMenuController {
/**
* Given a set of actions, update the menu.
*/
void setAppActions(ParceledListSlice<RemoteAction> appActions);
void setAppActions(ParceledListSlice<RemoteAction> appActions, RemoteAction closeAction);
/**
* Resize the PiP menu with the given bounds. The PiP SurfaceControl is given if there is a

View File

@@ -474,7 +474,8 @@ public class PhonePipMenuController implements PipMenuController {
* Sets the menu actions to the actions provided by the current PiP menu.
*/
@Override
public void setAppActions(ParceledListSlice<RemoteAction> appActions) {
public void setAppActions(ParceledListSlice<RemoteAction> appActions,
RemoteAction closeAction) {
mAppActions = appActions;
updateMenuActions();
}

View File

@@ -246,8 +246,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
@Override
public void onActionsChanged(ParceledListSlice<RemoteAction> actions) {
mMenuController.setAppActions(actions);
public void onActionsChanged(ParceledListSlice<RemoteAction> actions,
RemoteAction closeAction) {
mMenuController.setAppActions(actions, closeAction);
}
@Override

View File

@@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import android.annotation.IntDef;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.app.TaskInfo;
import android.content.ComponentName;
@@ -69,7 +70,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private static final int NONEXISTENT_TASK_ID = -1;
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = { "STATE_" }, value = {
@IntDef(prefix = {"STATE_"}, value = {
STATE_NO_PIP,
STATE_PIP,
STATE_PIP_MENU,
@@ -109,6 +110,10 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private int mPinnedTaskId = NONEXISTENT_TASK_ID;
private Runnable mUnstashRunnable;
private RemoteAction mCloseAction;
// How long the shell will wait for the app to close the PiP if a custom action is set.
private int mPipForceCloseDelay;
private int mResizeAnimationDuration;
public static Pip create(
@@ -369,9 +374,29 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
public void closePip() {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: closePip(), state=%s", TAG, stateToName(mState));
"%s: closePip(), state=%s, loseAction=%s", TAG, stateToName(mState),
mCloseAction);
}
if (mCloseAction != null) {
try {
mCloseAction.getActionIntent().send();
} catch (PendingIntent.CanceledException e) {
ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Failed to send close action, %s", TAG, e);
}
mMainExecutor.executeDelayed(() -> closeCurrentPiP(mPinnedTaskId), mPipForceCloseDelay);
} else {
closeCurrentPiP(mPinnedTaskId);
}
}
private void closeCurrentPiP(int pinnedTaskId) {
if (mPinnedTaskId != pinnedTaskId) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: PiP has already been closed by custom close action", TAG);
return;
}
removeTask(mPinnedTaskId);
onPipDisappeared();
}
@@ -468,6 +493,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private void loadConfigurations() {
final Resources res = mContext.getResources();
mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay);
}
private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) {
@@ -592,13 +618,15 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
public void onMovementBoundsChanged(boolean fromImeAdjustment) {}
@Override
public void onActionsChanged(ParceledListSlice<RemoteAction> actions) {
public void onActionsChanged(ParceledListSlice<RemoteAction> actions,
RemoteAction closeAction) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onActionsChanged()", TAG);
}
mTvPipMenuController.setAppActions(actions);
mTvPipMenuController.setAppActions(actions, closeAction);
mCloseAction = closeAction;
}
});
} catch (RemoteException e) {
@@ -617,7 +645,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: > taskInfo=%s", TAG, taskInfo);
"%s: taskInfo=%s", TAG, taskInfo);
}
return taskInfo;
} catch (RemoteException e) {

View File

@@ -126,4 +126,15 @@ public class TvPipMenuActionButton extends RelativeLayout implements View.OnClic
public boolean isEnabled() {
return mButtonView.isEnabled();
}
void setIsCustomCloseAction(boolean isCustomCloseAction) {
mIconImageView.setImageTintList(
getResources().getColorStateList(
isCustomCloseAction ? R.color.tv_pip_menu_close_icon
: R.color.tv_pip_menu_icon));
mButtonView.setBackgroundTintList(getResources()
.getColorStateList(isCustomCloseAction ? R.color.tv_pip_menu_close_icon_bg
: R.color.tv_pip_menu_icon_bg));
}
}

View File

@@ -45,6 +45,7 @@ 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.
@@ -67,6 +68,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
private final List<RemoteAction> mMediaActions = new ArrayList<>();
private final List<RemoteAction> mAppActions = new ArrayList<>();
private RemoteAction mCloseAction;
private SyncRtSurfaceTransactionApplier mApplier;
RectF mTmpSourceRectF = new RectF();
@@ -270,12 +272,12 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
}
@Override
public void setAppActions(ParceledListSlice<RemoteAction> actions) {
public void setAppActions(ParceledListSlice<RemoteAction> actions, RemoteAction closeAction) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: setAppActions()", TAG);
}
updateAdditionalActionsList(mAppActions, actions.getList());
updateAdditionalActionsList(mAppActions, actions.getList(), closeAction);
}
private void onMediaActionsChanged(List<RemoteAction> actions) {
@@ -291,17 +293,19 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
enabledActions.add(remoteAction);
}
}
updateAdditionalActionsList(mMediaActions, enabledActions);
updateAdditionalActionsList(mMediaActions, enabledActions, mCloseAction);
}
private void updateAdditionalActionsList(
List<RemoteAction> destination, @Nullable List<RemoteAction> source) {
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()) {
if (number == 0 && destination.isEmpty() && Objects.equals(closeAction, mCloseAction)) {
// Nothing changed.
return;
}
mCloseAction = closeAction;
destination.clear();
if (number > 0) {
destination.addAll(source);
@@ -314,9 +318,9 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
return;
}
if (!mAppActions.isEmpty()) {
mPipMenuView.setAdditionalActions(mAppActions, mMainHandler);
mPipMenuView.setAdditionalActions(mAppActions, mCloseAction, mMainHandler);
} else {
mPipMenuView.setAdditionalActions(mMediaActions, mMainHandler);
mPipMenuView.setAdditionalActions(mMediaActions, mCloseAction, mMainHandler);
}
}

View File

@@ -50,6 +50,7 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* A View that represents Pip Menu on TV. It's responsible for displaying 3 ever-present Pip Menu
@@ -78,6 +79,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
private Rect mCurrentBounds;
private final TvPipMenuActionButton mExpandButton;
private final TvPipMenuActionButton mCloseButton;
public TvPipMenuView(@NonNull Context context) {
this(context, null);
@@ -100,8 +102,11 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons);
mActionButtonsContainer.findViewById(R.id.tv_pip_menu_fullscreen_button)
.setOnClickListener(this);
mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button)
.setOnClickListener(this);
mCloseButton = mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button);
mCloseButton.setOnClickListener(this);
mCloseButton.setIsCustomCloseAction(true);
mActionButtonsContainer.findViewById(R.id.tv_pip_menu_move_button)
.setOnClickListener(this);
mExpandButton = findViewById(R.id.tv_pip_menu_expand_button);
@@ -220,12 +225,24 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|| mArrowLeft.getAlpha() != 0f;
}
void setAdditionalActions(List<RemoteAction> actions, Handler mainHandler) {
void setAdditionalActions(List<RemoteAction> actions, RemoteAction closeAction,
Handler mainHandler) {
if (DEBUG) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: setAdditionalActions()", TAG);
}
// Replace system close action with custom close action if available
if (closeAction != null) {
setActionForButton(closeAction, mCloseButton, mainHandler);
} else {
mCloseButton.setTextAndDescription(R.string.pip_close);
mCloseButton.setImageResource(R.drawable.pip_ic_close_white);
}
mCloseButton.setIsCustomCloseAction(closeAction != null);
// Make sure the close action is always enabled
mCloseButton.setEnabled(true);
// Make sure we exactly as many additional buttons as we have actions to display.
final int actionsNumber = actions.size();
int buttonsNumber = mAdditionalButtons.size();
@@ -256,14 +273,37 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
for (int index = 0; index < actionsNumber; index++) {
final RemoteAction action = actions.get(index);
final TvPipMenuActionButton button = mAdditionalButtons.get(index);
button.setVisibility(View.VISIBLE); // Ensure the button is visible.
button.setTextAndDescription(action.getContentDescription());
button.setEnabled(action.isEnabled());
button.setTag(action);
action.getIcon().loadDrawableAsync(mContext, button::setImageDrawable, mainHandler);
// Remove action if it matches the custom close action.
if (actionsMatch(action, closeAction)) {
button.setVisibility(GONE);
continue;
}
setActionForButton(action, button, mainHandler);
}
}
/**
* Checks whether title, description and intent match.
* Comparing icons would be good, but using equals causes false negatives
*/
private boolean actionsMatch(RemoteAction action1, RemoteAction action2) {
if (action1 == action2) return true;
if (action1 == null) return false;
return Objects.equals(action1.getTitle(), action2.getTitle())
&& Objects.equals(action1.getContentDescription(), action2.getContentDescription())
&& Objects.equals(action1.getActionIntent(), action2.getActionIntent());
}
private void setActionForButton(RemoteAction action, TvPipMenuActionButton button,
Handler mainHandler) {
button.setVisibility(View.VISIBLE); // Ensure the button is visible.
button.setTextAndDescription(action.getContentDescription());
button.setEnabled(action.isEnabled());
button.setTag(action);
action.getIcon().loadDrawableAsync(mContext, button::setImageDrawable, mainHandler);
}
@Nullable
SurfaceControl getWindowSurfaceControl() {
final ViewRootImpl root = getViewRootImpl();

View File

@@ -749,7 +749,8 @@ class ActivityClientController extends IActivityClientController.Stub {
rootTask.setPictureInPictureAspectRatio(
r.pictureInPictureArgs.getAspectRatioFloat(),
r.pictureInPictureArgs.getExpandedAspectRatioFloat());
rootTask.setPictureInPictureActions(r.pictureInPictureArgs.getActions());
rootTask.setPictureInPictureActions(r.pictureInPictureArgs.getActions(),
r.pictureInPictureArgs.getCloseAction());
}
}
} finally {

View File

@@ -3527,11 +3527,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
final float expandedAspectRatio =
r.pictureInPictureArgs.getExpandedAspectRatioFloat();
final List<RemoteAction> actions = r.pictureInPictureArgs.getActions();
final RemoteAction closeAction = r.pictureInPictureArgs.getCloseAction();
mRootWindowContainer.moveActivityToPinnedRootTask(r,
null /* launchIntoPipHostActivity */, "enterPictureInPictureMode");
final Task task = r.getTask();
task.setPictureInPictureAspectRatio(aspectRatio, expandedAspectRatio);
task.setPictureInPictureActions(actions);
task.setPictureInPictureActions(actions, closeAction);
// Continue the pausing process after entering pip.
if (task.getPausingActivity() == r) {

View File

@@ -92,6 +92,7 @@ class PinnedTaskController {
// The set of actions and aspect-ratio for the that are currently allowed on the PiP activity
private ArrayList<RemoteAction> mActions = new ArrayList<>();
private RemoteAction mCloseAction;
private float mAspectRatio = -1f;
private float mExpandedAspectRatio = 0f;
@@ -154,7 +155,7 @@ class PinnedTaskController {
mPinnedTaskListener = listener;
notifyImeVisibilityChanged(mIsImeShowing, mImeHeight);
notifyMovementBoundsChanged(false /* fromImeAdjustment */);
notifyActionsChanged(mActions);
notifyActionsChanged(mActions, mCloseAction);
} catch (RemoteException e) {
Log.e(TAG, "Failed to register pinned task listener", e);
}
@@ -408,12 +409,13 @@ class PinnedTaskController {
/**
* Sets the current set of actions.
*/
void setActions(List<RemoteAction> actions) {
void setActions(List<RemoteAction> actions, RemoteAction closeAction) {
mActions.clear();
if (actions != null) {
mActions.addAll(actions);
}
notifyActionsChanged(mActions);
mCloseAction = closeAction;
notifyActionsChanged(mActions, closeAction);
}
/**
@@ -450,10 +452,10 @@ class PinnedTaskController {
/**
* Notifies listeners that the PIP actions have changed.
*/
private void notifyActionsChanged(List<RemoteAction> actions) {
private void notifyActionsChanged(List<RemoteAction> actions, RemoteAction closeAction) {
if (mPinnedTaskListener != null) {
try {
mPinnedTaskListener.onActionsChanged(new ParceledListSlice(actions));
mPinnedTaskListener.onActionsChanged(new ParceledListSlice(actions), closeAction);
} catch (RemoteException e) {
Slog.e(TAG_WM, "Error delivering actions changed event.", e);
}

View File

@@ -6098,7 +6098,7 @@ class Task extends TaskFragment {
/**
* Sets the current picture-in-picture actions.
*/
void setPictureInPictureActions(List<RemoteAction> actions) {
void setPictureInPictureActions(List<RemoteAction> actions, RemoteAction closeAction) {
if (!mWmService.mAtmService.mSupportsPictureInPicture) {
return;
}
@@ -6107,7 +6107,7 @@ class Task extends TaskFragment {
return;
}
getDisplayContent().getPinnedTaskController().setActions(actions);
getDisplayContent().getPinnedTaskController().setActions(actions, closeAction);
}
public DisplayInfo getDisplayInfo() {