Merge changes I2b616125,Ibf892d7d,Idbe4c2ab
* changes: Unify TvPipAction execution code path. Handle all TvPipActions within one component. Use RecyclerView for TV PiP menu buttons.
This commit is contained in:
committed by
Android (Google) Code Review
commit
912f380ce8
@@ -44,67 +44,15 @@
|
||||
android:background="@color/tv_pip_menu_dim_layer"
|
||||
android:alpha="0"/>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/tv_pip_menu_scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="none"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/tv_pip_menu_horizontal_scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="none">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/tv_pip_menu_action_buttons"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:alpha="0">
|
||||
|
||||
<Space
|
||||
android:layout_width="@dimen/pip_menu_button_wrapper_margin"
|
||||
android:layout_height="@dimen/pip_menu_button_wrapper_margin"/>
|
||||
|
||||
<com.android.wm.shell.common.TvWindowMenuActionButton
|
||||
android:id="@+id/tv_pip_menu_fullscreen_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/pip_ic_fullscreen_white"
|
||||
android:text="@string/pip_fullscreen" />
|
||||
|
||||
<com.android.wm.shell.common.TvWindowMenuActionButton
|
||||
android:id="@+id/tv_pip_menu_close_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/pip_ic_close_white"
|
||||
android:text="@string/pip_close" />
|
||||
|
||||
<!-- More TvWindowMenuActionButtons may be added here at runtime. -->
|
||||
|
||||
<com.android.wm.shell.common.TvWindowMenuActionButton
|
||||
android:id="@+id/tv_pip_menu_move_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/pip_ic_move_white"
|
||||
android:text="@string/pip_move" />
|
||||
|
||||
<com.android.wm.shell.common.TvWindowMenuActionButton
|
||||
android:id="@+id/tv_pip_menu_expand_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/pip_ic_collapse"
|
||||
android:visibility="gone"
|
||||
android:text="@string/pip_collapse" />
|
||||
|
||||
<Space
|
||||
android:layout_width="@dimen/pip_menu_button_wrapper_margin"
|
||||
android:layout_height="@dimen/pip_menu_button_wrapper_margin"/>
|
||||
|
||||
</LinearLayout>
|
||||
</HorizontalScrollView>
|
||||
<com.android.internal.widget.RecyclerView
|
||||
android:id="@+id/tv_pip_menu_action_buttons"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:padding="@dimen/pip_menu_button_start_end_offset"
|
||||
android:clipToPadding="false"
|
||||
android:alpha="0"
|
||||
android:contentDescription="@string/a11y_pip_menu_entered"/>
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Frame around the content, just overlapping the corners to make them round -->
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
android:layout_width="@dimen/tv_window_menu_button_size"
|
||||
android:layout_height="@dimen/tv_window_menu_button_size"
|
||||
android:padding="@dimen/tv_window_menu_button_margin"
|
||||
android:duplicateParentState="true"
|
||||
android:stateListAnimator="@animator/tv_window_menu_action_button_animator"
|
||||
android:focusable="true">
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<dimen name="pip_menu_background_corner_radius">6dp</dimen>
|
||||
<dimen name="pip_menu_border_width">4dp</dimen>
|
||||
<dimen name="pip_menu_outer_space">24dp</dimen>
|
||||
<dimen name="pip_menu_button_wrapper_margin">26dp</dimen>
|
||||
<dimen name="pip_menu_button_start_end_offset">30dp</dimen>
|
||||
|
||||
<!-- outer space minus border width -->
|
||||
<dimen name="pip_menu_outer_space_frame">20dp</dimen>
|
||||
|
||||
@@ -19,6 +19,8 @@ package com.android.wm.shell.common;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Handler;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -30,11 +32,11 @@ import com.android.wm.shell.R;
|
||||
/**
|
||||
* A common action button for TV window menu layouts.
|
||||
*/
|
||||
public class TvWindowMenuActionButton extends RelativeLayout implements View.OnClickListener {
|
||||
public class TvWindowMenuActionButton extends RelativeLayout {
|
||||
private final ImageView mIconImageView;
|
||||
private final View mButtonBackgroundView;
|
||||
private final View mButtonView;
|
||||
private OnClickListener mOnClickListener;
|
||||
|
||||
private Icon mCurrentIcon;
|
||||
|
||||
public TvWindowMenuActionButton(Context context) {
|
||||
this(context, null, 0, 0);
|
||||
@@ -56,7 +58,6 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC
|
||||
inflater.inflate(R.layout.tv_window_menu_action_button, this);
|
||||
|
||||
mIconImageView = findViewById(R.id.icon);
|
||||
mButtonView = findViewById(R.id.button);
|
||||
mButtonBackgroundView = findViewById(R.id.background);
|
||||
|
||||
final int[] values = new int[]{android.R.attr.src, android.R.attr.text};
|
||||
@@ -71,23 +72,6 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC
|
||||
typedArray.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnClickListener(OnClickListener listener) {
|
||||
// We do not want to set an OnClickListener to the TvWindowMenuActionButton itself, but only
|
||||
// to the ImageView. So let's "cash" the listener we've been passed here and set a "proxy"
|
||||
// listener to the ImageView.
|
||||
mOnClickListener = listener;
|
||||
mButtonView.setOnClickListener(listener != null ? this : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mOnClickListener != null) {
|
||||
// Pass the correct view - this.
|
||||
mOnClickListener.onClick(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the drawable for the button with the given drawable.
|
||||
*/
|
||||
@@ -104,11 +88,24 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC
|
||||
}
|
||||
}
|
||||
|
||||
public void setImageIconAsync(Icon icon, Handler handler) {
|
||||
mCurrentIcon = icon;
|
||||
// Remove old image while waiting for the new one to load.
|
||||
mIconImageView.setImageDrawable(null);
|
||||
icon.loadDrawableAsync(mContext, d -> {
|
||||
// The image hasn't been set any other way and the drawable belongs to the most
|
||||
// recently set Icon.
|
||||
if (mIconImageView.getDrawable() == null && mCurrentIcon == icon) {
|
||||
mIconImageView.setImageDrawable(d);
|
||||
}
|
||||
}, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text for description the with the given string.
|
||||
*/
|
||||
public void setTextAndDescription(CharSequence text) {
|
||||
mButtonView.setContentDescription(text);
|
||||
setContentDescription(text);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,16 +115,6 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC
|
||||
setTextAndDescription(getContext().getString(resId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
mButtonView.setEnabled(enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return mButtonView.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this button as a custom close action button.
|
||||
* This changes the style of the action button to highlight that this action finishes the
|
||||
@@ -147,10 +134,10 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (mButtonView.getContentDescription() == null) {
|
||||
if (getContentDescription() == null) {
|
||||
return TvWindowMenuActionButton.class.getSimpleName();
|
||||
}
|
||||
return mButtonView.getContentDescription().toString();
|
||||
return getContentDescription().toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ public abstract class TvPipModule {
|
||||
PipParamsChangedForwarder pipParamsChangedForwarder,
|
||||
DisplayController displayController,
|
||||
WindowManagerShellWrapper windowManagerShellWrapper,
|
||||
@ShellMainThread Handler mainHandler, // needed for registerReceiverForAllUsers()
|
||||
@ShellMainThread ShellExecutor mainExecutor) {
|
||||
return Optional.of(
|
||||
TvPipController.create(
|
||||
@@ -100,6 +101,7 @@ public abstract class TvPipModule {
|
||||
pipParamsChangedForwarder,
|
||||
displayController,
|
||||
windowManagerShellWrapper,
|
||||
mainHandler,
|
||||
mainExecutor));
|
||||
}
|
||||
|
||||
@@ -157,22 +159,17 @@ public abstract class TvPipModule {
|
||||
Context context,
|
||||
TvPipBoundsState tvPipBoundsState,
|
||||
SystemWindows systemWindows,
|
||||
PipMediaController pipMediaController,
|
||||
@ShellMainThread Handler mainHandler) {
|
||||
return new TvPipMenuController(context, tvPipBoundsState, systemWindows, pipMediaController,
|
||||
mainHandler);
|
||||
return new TvPipMenuController(context, tvPipBoundsState, systemWindows, mainHandler);
|
||||
}
|
||||
|
||||
// Handler needed for registerReceiverForAllUsers()
|
||||
@WMSingleton
|
||||
@Provides
|
||||
static TvPipNotificationController provideTvPipNotificationController(Context context,
|
||||
PipMediaController pipMediaController,
|
||||
PipParamsChangedForwarder pipParamsChangedForwarder,
|
||||
TvPipBoundsState tvPipBoundsState,
|
||||
@ShellMainThread Handler mainHandler) {
|
||||
PipParamsChangedForwarder pipParamsChangedForwarder) {
|
||||
return new TvPipNotificationController(context, pipMediaController,
|
||||
pipParamsChangedForwarder, tvPipBoundsState, mainHandler);
|
||||
pipParamsChangedForwarder);
|
||||
}
|
||||
|
||||
@WMSingleton
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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 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.wm.shell.common.TvWindowMenuActionButton;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Objects;
|
||||
|
||||
abstract class TvPipAction {
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(prefix = {"ACTION_"}, value = {
|
||||
ACTION_FULLSCREEN,
|
||||
ACTION_CLOSE,
|
||||
ACTION_MOVE,
|
||||
ACTION_EXPAND_COLLAPSE,
|
||||
ACTION_CUSTOM,
|
||||
ACTION_CUSTOM_CLOSE
|
||||
})
|
||||
public @interface ActionType {
|
||||
}
|
||||
|
||||
public static final int ACTION_FULLSCREEN = 0;
|
||||
public static final int ACTION_CLOSE = 1;
|
||||
public static final int ACTION_MOVE = 2;
|
||||
public static final int ACTION_EXPAND_COLLAPSE = 3;
|
||||
public static final int ACTION_CUSTOM = 4;
|
||||
public static final int ACTION_CUSTOM_CLOSE = 5;
|
||||
|
||||
@ActionType
|
||||
private final int mActionType;
|
||||
|
||||
@NonNull
|
||||
private final SystemActionsHandler mSystemActionsHandler;
|
||||
|
||||
TvPipAction(@ActionType int actionType, @NonNull SystemActionsHandler systemActionsHandler) {
|
||||
Objects.requireNonNull(systemActionsHandler);
|
||||
mActionType = actionType;
|
||||
mSystemActionsHandler = systemActionsHandler;
|
||||
}
|
||||
|
||||
boolean isCloseAction() {
|
||||
return mActionType == ACTION_CLOSE || mActionType == ACTION_CUSTOM_CLOSE;
|
||||
}
|
||||
|
||||
@ActionType
|
||||
int getActionType() {
|
||||
return mActionType;
|
||||
}
|
||||
|
||||
abstract void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler);
|
||||
|
||||
abstract PendingIntent getPendingIntent();
|
||||
|
||||
void executeAction() {
|
||||
mSystemActionsHandler.executeAction(mActionType);
|
||||
}
|
||||
|
||||
abstract Notification.Action toNotificationAction(Context context);
|
||||
|
||||
interface SystemActionsHandler {
|
||||
void executeAction(@TvPipAction.ActionType int actionType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* 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.TvPipController.ACTION_CLOSE_PIP;
|
||||
import static com.android.wm.shell.pip.tv.TvPipController.ACTION_MOVE_PIP;
|
||||
import static com.android.wm.shell.pip.tv.TvPipController.ACTION_TOGGLE_EXPANDED_PIP;
|
||||
import static com.android.wm.shell.pip.tv.TvPipController.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 implements TvPipAction.SystemActionsHandler {
|
||||
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 TvPipAction.SystemActionsHandler mSystemActionsHandler;
|
||||
|
||||
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,
|
||||
TvPipAction.SystemActionsHandler systemActionsHandler) {
|
||||
mSystemActionsHandler = systemActionsHandler;
|
||||
|
||||
mActionsList = new ArrayList<>();
|
||||
mActionsList.add(new TvPipSystemAction(ACTION_FULLSCREEN, R.string.pip_fullscreen,
|
||||
R.drawable.pip_ic_fullscreen_white, ACTION_TO_FULLSCREEN, context,
|
||||
mSystemActionsHandler));
|
||||
|
||||
mDefaultCloseAction = new TvPipSystemAction(ACTION_CLOSE, R.string.pip_close,
|
||||
R.drawable.pip_ic_close_white, ACTION_CLOSE_PIP, context, mSystemActionsHandler);
|
||||
mActionsList.add(mDefaultCloseAction);
|
||||
|
||||
mActionsList.add(new TvPipSystemAction(ACTION_MOVE, R.string.pip_move,
|
||||
R.drawable.pip_ic_move_white, ACTION_MOVE_PIP, context, mSystemActionsHandler));
|
||||
|
||||
mExpandCollapseAction = new TvPipSystemAction(ACTION_EXPAND_COLLAPSE, R.string.pip_collapse,
|
||||
R.drawable.pip_ic_collapse, ACTION_TOGGLE_EXPANDED_PIP, context,
|
||||
mSystemActionsHandler);
|
||||
mActionsList.add(mExpandCollapseAction);
|
||||
|
||||
pipMediaController.addActionListener(this::onMediaActionsChanged);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeAction(@TvPipAction.ActionType int actionType) {
|
||||
if (mSystemActionsHandler != null) {
|
||||
mSystemActionsHandler.executeAction(actionType);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
mSystemActionsHandler));
|
||||
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, mSystemActionsHandler));
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -22,13 +22,16 @@ 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.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Handler;
|
||||
import android.os.RemoteException;
|
||||
import android.view.Gravity;
|
||||
|
||||
@@ -67,8 +70,8 @@ import java.util.Set;
|
||||
*/
|
||||
public class TvPipController implements PipTransitionController.PipTransitionCallback,
|
||||
TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate,
|
||||
TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener,
|
||||
ConfigurationChangeListener, UserChangeListener {
|
||||
DisplayController.OnDisplaysChangedListener, ConfigurationChangeListener,
|
||||
UserChangeListener {
|
||||
private static final String TAG = "TvPipController";
|
||||
|
||||
private static final int NONEXISTENT_TASK_ID = -1;
|
||||
@@ -98,6 +101,17 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
*/
|
||||
private static final int STATE_PIP_MENU = 2;
|
||||
|
||||
static final String ACTION_SHOW_PIP_MENU =
|
||||
"com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU";
|
||||
static final String ACTION_CLOSE_PIP =
|
||||
"com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP";
|
||||
static final String ACTION_MOVE_PIP =
|
||||
"com.android.wm.shell.pip.tv.notification.action.MOVE_PIP";
|
||||
static final String ACTION_TOGGLE_EXPANDED_PIP =
|
||||
"com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP";
|
||||
static final String ACTION_TO_FULLSCREEN =
|
||||
"com.android.wm.shell.pip.tv.notification.action.FULLSCREEN";
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private final ShellController mShellController;
|
||||
@@ -107,6 +121,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;
|
||||
@@ -115,14 +130,16 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
private final DisplayController mDisplayController;
|
||||
private final WindowManagerShellWrapper mWmShellWrapper;
|
||||
private final ShellExecutor mMainExecutor;
|
||||
private final Handler mMainHandler; // For registering the broadcast receiver
|
||||
private final TvPipImpl mImpl = new TvPipImpl();
|
||||
|
||||
private final ActionBroadcastReceiver mActionBroadcastReceiver;
|
||||
|
||||
@State
|
||||
private int mState = STATE_NO_PIP;
|
||||
private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY;
|
||||
private int mPinnedTaskId = NONEXISTENT_TASK_ID;
|
||||
|
||||
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;
|
||||
|
||||
@@ -146,6 +163,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
PipParamsChangedForwarder pipParamsChangedForwarder,
|
||||
DisplayController displayController,
|
||||
WindowManagerShellWrapper wmShell,
|
||||
Handler mainHandler,
|
||||
ShellExecutor mainExecutor) {
|
||||
return new TvPipController(
|
||||
context,
|
||||
@@ -164,6 +182,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
pipParamsChangedForwarder,
|
||||
displayController,
|
||||
wmShell,
|
||||
mainHandler,
|
||||
mainExecutor).mImpl;
|
||||
}
|
||||
|
||||
@@ -184,8 +203,10 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
PipParamsChangedForwarder pipParamsChangedForwarder,
|
||||
DisplayController displayController,
|
||||
WindowManagerShellWrapper wmShellWrapper,
|
||||
Handler mainHandler,
|
||||
ShellExecutor mainExecutor) {
|
||||
mContext = context;
|
||||
mMainHandler = mainHandler;
|
||||
mMainExecutor = mainExecutor;
|
||||
mShellController = shellController;
|
||||
mDisplayController = displayController;
|
||||
@@ -198,12 +219,17 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
mTvPipBoundsController.setListener(this);
|
||||
|
||||
mPipMediaController = pipMediaController;
|
||||
mTvPipActionsProvider = new TvPipActionsProvider(context, pipMediaController,
|
||||
this::executeAction);
|
||||
|
||||
mPipNotificationController = pipNotificationController;
|
||||
mPipNotificationController.setDelegate(this);
|
||||
mPipNotificationController.setTvPipActionsProvider(mTvPipActionsProvider);
|
||||
|
||||
mTvPipMenuController = tvPipMenuController;
|
||||
mTvPipMenuController.setDelegate(this);
|
||||
mTvPipMenuController.setTvPipActionsProvider(mTvPipActionsProvider);
|
||||
|
||||
mActionBroadcastReceiver = new ActionBroadcastReceiver();
|
||||
|
||||
mAppOpsListener = pipAppOpsListener;
|
||||
mPipTaskOrganizer = pipTaskOrganizer;
|
||||
@@ -241,7 +267,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
"%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState));
|
||||
|
||||
loadConfigurations();
|
||||
mPipNotificationController.onConfigurationChanged(mContext);
|
||||
mPipNotificationController.onConfigurationChanged();
|
||||
mTvPipBoundsAlgorithm.onConfigurationChanged(mContext);
|
||||
}
|
||||
|
||||
@@ -256,9 +282,10 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
* Starts the process if bringing up the Pip menu if by issuing a command to move Pip
|
||||
* task/window to the "Menu" position. We'll show the actual Menu UI (eg. actions) once the Pip
|
||||
* task/window is properly positioned in {@link #onPipTransitionFinished(int)}.
|
||||
*
|
||||
* @param moveMenu If true, show the moveMenu, otherwise show the regular menu.
|
||||
*/
|
||||
@Override
|
||||
public void showPictureInPictureMenu() {
|
||||
private void showPictureInPictureMenu(boolean moveMenu) {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: showPictureInPictureMenu(), state=%s", TAG, stateToName(mState));
|
||||
|
||||
@@ -269,7 +296,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
}
|
||||
|
||||
setState(STATE_PIP_MENU);
|
||||
mTvPipMenuController.showMenu();
|
||||
if (moveMenu) {
|
||||
mTvPipMenuController.showMovementMenu();
|
||||
} else {
|
||||
mTvPipMenuController.showMenu();
|
||||
}
|
||||
updatePinnedStackBounds();
|
||||
}
|
||||
|
||||
@@ -289,8 +320,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
/**
|
||||
* Opens the "Pip-ed" Activity fullscreen.
|
||||
*/
|
||||
@Override
|
||||
public void movePipToFullscreen() {
|
||||
private void movePipToFullscreen() {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState));
|
||||
|
||||
@@ -298,8 +328,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
onPipDisappeared();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void togglePipExpansion() {
|
||||
private void togglePipExpansion() {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: togglePipExpansion()", TAG);
|
||||
boolean expanding = !mTvPipBoundsState.isTvPipExpanded();
|
||||
@@ -310,17 +339,10 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
}
|
||||
mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding);
|
||||
mTvPipBoundsState.setTvPipExpanded(expanding);
|
||||
mPipNotificationController.updateExpansionState();
|
||||
|
||||
updatePinnedStackBounds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enterPipMovementMenu() {
|
||||
setState(STATE_PIP_MENU);
|
||||
mTvPipMenuController.showMovementMenuOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void movePip(int keycode) {
|
||||
if (mTvPipBoundsAlgorithm.updateGravity(keycode)) {
|
||||
@@ -373,30 +395,23 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes Pip window.
|
||||
*/
|
||||
@Override
|
||||
public void closePip() {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: closePip(), state=%s, loseAction=%s", TAG, stateToName(mState),
|
||||
mCloseAction);
|
||||
closeCurrentPiP(mPinnedTaskId);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
/**
|
||||
* Force close the current PiP after some time in case the custom action hasn't done it by
|
||||
* itself.
|
||||
*/
|
||||
public void customClosePip() {
|
||||
mMainExecutor.executeDelayed(() -> closeCurrentPiP(mPinnedTaskId), mPipForceCloseDelay);
|
||||
}
|
||||
|
||||
private void closeCurrentPiP(int pinnedTaskId) {
|
||||
@@ -426,6 +441,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
mPinnedTaskId = pinnedTask.taskId;
|
||||
|
||||
mPipMediaController.onActivityPinned();
|
||||
mActionBroadcastReceiver.register();
|
||||
mPipNotificationController.show(pinnedTask.topActivity.getPackageName());
|
||||
}
|
||||
|
||||
@@ -445,6 +461,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
"%s: onPipDisappeared() state=%s", TAG, stateToName(mState));
|
||||
|
||||
mPipNotificationController.dismiss();
|
||||
mActionBroadcastReceiver.unregister();
|
||||
|
||||
mTvPipMenuController.closeMenu();
|
||||
mTvPipBoundsState.resetTvPipState();
|
||||
mTvPipBoundsController.onPipDismissed();
|
||||
@@ -454,6 +472,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 +488,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 +501,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,8 +564,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: onActionsChanged()", TAG);
|
||||
|
||||
mTvPipMenuController.setAppActions(actions, closeAction);
|
||||
mCloseAction = closeAction;
|
||||
mTvPipActionsProvider.setAppActions(actions, closeAction);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -555,7 +584,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
|
||||
@@ -662,6 +691,90 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
||||
}
|
||||
}
|
||||
|
||||
private void executeAction(@TvPipAction.ActionType int actionType) {
|
||||
switch (actionType) {
|
||||
case TvPipAction.ACTION_FULLSCREEN:
|
||||
movePipToFullscreen();
|
||||
break;
|
||||
case TvPipAction.ACTION_CLOSE:
|
||||
closePip();
|
||||
break;
|
||||
case TvPipAction.ACTION_MOVE:
|
||||
showPictureInPictureMenu(/* moveMenu= */ true);
|
||||
break;
|
||||
case TvPipAction.ACTION_CUSTOM_CLOSE:
|
||||
customClosePip();
|
||||
break;
|
||||
case TvPipAction.ACTION_EXPAND_COLLAPSE:
|
||||
togglePipExpansion();
|
||||
break;
|
||||
default:
|
||||
// NOOP
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private class ActionBroadcastReceiver extends BroadcastReceiver {
|
||||
private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";
|
||||
|
||||
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_TO_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)) {
|
||||
showPictureInPictureMenu(/* moveMenu= */ false);
|
||||
} else {
|
||||
executeAction(getCorrespondingActionType(action));
|
||||
}
|
||||
}
|
||||
|
||||
@TvPipAction.ActionType
|
||||
private int getCorrespondingActionType(String broadcast) {
|
||||
if (ACTION_CLOSE_PIP.equals(broadcast)) {
|
||||
return TvPipAction.ACTION_CLOSE;
|
||||
} else if (ACTION_MOVE_PIP.equals(broadcast)) {
|
||||
return TvPipAction.ACTION_MOVE;
|
||||
} else if (ACTION_TOGGLE_EXPANDED_PIP.equals(broadcast)) {
|
||||
return TvPipAction.ACTION_EXPAND_COLLAPSE;
|
||||
} else if (ACTION_TO_FULLSCREEN.equals(broadcast)) {
|
||||
return TvPipAction.ACTION_FULLSCREEN;
|
||||
}
|
||||
|
||||
// Default: handle it like an action we don't know the content of.
|
||||
return TvPipAction.ACTION_CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private class TvPipImpl implements Pip {
|
||||
// Not used
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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.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.internal.protolog.common.ProtoLog;
|
||||
import com.android.wm.shell.common.TvWindowMenuActionButton;
|
||||
import com.android.wm.shell.protolog.ShellProtoLogGroup;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A TvPipAction for actions that the app provides via {@link
|
||||
* android.app.PictureInPictureParams.Builder#setCloseAction(RemoteAction)} or {@link
|
||||
* android.app.PictureInPictureParams.Builder#setActions(List)}.
|
||||
*/
|
||||
public class TvPipCustomAction extends TvPipAction {
|
||||
private static final String TAG = TvPipCustomAction.class.getSimpleName();
|
||||
|
||||
private final RemoteAction mRemoteAction;
|
||||
|
||||
TvPipCustomAction(@ActionType int actionType, @NonNull RemoteAction remoteAction,
|
||||
SystemActionsHandler systemActionsHandler) {
|
||||
super(actionType, systemActionsHandler);
|
||||
Objects.requireNonNull(remoteAction);
|
||||
mRemoteAction = remoteAction;
|
||||
}
|
||||
|
||||
void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler) {
|
||||
if (button == null || mainHandler == null) return;
|
||||
if (mRemoteAction.getContentDescription().length() > 0) {
|
||||
button.setTextAndDescription(mRemoteAction.getContentDescription());
|
||||
} else {
|
||||
button.setTextAndDescription(mRemoteAction.getTitle());
|
||||
}
|
||||
button.setImageIconAsync(mRemoteAction.getIcon(), mainHandler);
|
||||
button.setEnabled(isCloseAction() || mRemoteAction.isEnabled());
|
||||
}
|
||||
|
||||
PendingIntent getPendingIntent() {
|
||||
return mRemoteAction.getActionIntent();
|
||||
}
|
||||
|
||||
void executeAction() {
|
||||
super.executeAction();
|
||||
try {
|
||||
mRemoteAction.getActionIntent().send();
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: Failed to send action, %s", TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,22 +57,20 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
private final SystemWindows mSystemWindows;
|
||||
private final TvPipBoundsState mTvPipBoundsState;
|
||||
private final Handler mMainHandler;
|
||||
private TvPipActionsProvider mTvPipActionsProvider;
|
||||
|
||||
private Delegate mDelegate;
|
||||
private SurfaceControl mLeash;
|
||||
private TvPipMenuView mPipMenuView;
|
||||
private View mPipBackgroundView;
|
||||
|
||||
private boolean mMenuIsOpen;
|
||||
// User can actively move the PiP via the DPAD.
|
||||
private boolean mInMoveMode;
|
||||
// Used when only showing the move menu since we want to close the menu completely when
|
||||
// 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,8 +78,7 @@ 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) {
|
||||
mContext = context;
|
||||
mTvPipBoundsState = tvPipBoundsState;
|
||||
mSystemWindows = systemWindows;
|
||||
@@ -101,9 +95,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) {
|
||||
@@ -120,6 +111,10 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
mDelegate = delegate;
|
||||
}
|
||||
|
||||
void setTvPipActionsProvider(TvPipActionsProvider tvPipActionsProvider) {
|
||||
mTvPipActionsProvider = tvPipActionsProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attach(SurfaceControl leash) {
|
||||
if (mDelegate == null) {
|
||||
@@ -146,15 +141,19 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
int pipMenuBorderWidth = mContext.getResources()
|
||||
.getDimensionPixelSize(R.dimen.pip_menu_border_width);
|
||||
mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipMenuBorderWidth,
|
||||
-pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth));
|
||||
-pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth));
|
||||
mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -pipEduTextHeight));
|
||||
}
|
||||
|
||||
private void attachPipMenuView() {
|
||||
mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this);
|
||||
if (mTvPipActionsProvider == null) {
|
||||
ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: Actions provider is not set", TAG);
|
||||
return;
|
||||
}
|
||||
mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this, mTvPipActionsProvider);
|
||||
setUpViewSurfaceZOrder(mPipMenuView, 1);
|
||||
addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE);
|
||||
maybeUpdateMenuViewActions();
|
||||
}
|
||||
|
||||
private void attachPipBackgroundView() {
|
||||
@@ -189,17 +188,22 @@ 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void showMovementMenuOnly() {
|
||||
void showMovementMenu() {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: showMovementMenuOnly()", TAG);
|
||||
setInMoveMode(true);
|
||||
mCloseAfterExitMoveMenu = true;
|
||||
showMenuInternal();
|
||||
if (mMenuIsOpen) {
|
||||
mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
|
||||
} else {
|
||||
mCloseAfterExitMoveMenu = true;
|
||||
showMenuInternal();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -214,14 +218,13 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
if (mPipMenuView == null) {
|
||||
return;
|
||||
}
|
||||
maybeUpdateMenuViewActions();
|
||||
updateExpansionState();
|
||||
|
||||
mMenuIsOpen = true;
|
||||
grantPipMenuFocus(true);
|
||||
if (mInMoveMode) {
|
||||
mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
|
||||
} else {
|
||||
mPipMenuView.showButtonsMenu();
|
||||
mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ false);
|
||||
}
|
||||
mPipMenuView.updateBounds(mTvPipBoundsState.getBounds());
|
||||
}
|
||||
@@ -236,11 +239,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);
|
||||
}
|
||||
@@ -252,6 +250,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
if (mPipMenuView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mMenuIsOpen = false;
|
||||
mPipMenuView.hideAllUserControls();
|
||||
grantPipMenuFocus(false);
|
||||
mDelegate.onMenuClosed();
|
||||
@@ -271,30 +271,20 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnterMoveMode() {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: onEnterMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode,
|
||||
mCloseAfterExitMoveMenu);
|
||||
setInMoveMode(true);
|
||||
mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onExitMoveMode() {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: onExitMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode,
|
||||
mCloseAfterExitMoveMenu);
|
||||
"%s: onExitMoveMode - %b, close when exiting move menu: %b",
|
||||
TAG, mInMoveMode, mCloseAfterExitMoveMenu);
|
||||
|
||||
if (mCloseAfterExitMoveMenu) {
|
||||
setInMoveMode(false);
|
||||
mCloseAfterExitMoveMenu = false;
|
||||
closeMenu();
|
||||
return true;
|
||||
}
|
||||
if (mInMoveMode) {
|
||||
setInMoveMode(false);
|
||||
mPipMenuView.showButtonsMenu();
|
||||
if (mCloseAfterExitMoveMenu) {
|
||||
mCloseAfterExitMoveMenu = false;
|
||||
closeMenu();
|
||||
} else {
|
||||
mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -319,51 +309,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, mMainHandler);
|
||||
} else {
|
||||
mPipMenuView.setAdditionalActions(mMediaActions, mCloseAction, mMainHandler);
|
||||
}
|
||||
// NOOP - handled via the TvPipActionsProvider
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -543,21 +489,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCloseButtonClick() {
|
||||
mDelegate.closePip();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFullscreenButtonClick() {
|
||||
mDelegate.movePipToFullscreen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToggleExpandedMode() {
|
||||
mDelegate.togglePipExpansion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCloseEduText() {
|
||||
mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
|
||||
@@ -565,21 +496,15 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
}
|
||||
|
||||
interface Delegate {
|
||||
void movePipToFullscreen();
|
||||
|
||||
void movePip(int keycode);
|
||||
|
||||
void onInMoveModeChanged();
|
||||
|
||||
int getPipGravity();
|
||||
|
||||
void togglePipExpansion();
|
||||
|
||||
void onMenuClosed();
|
||||
|
||||
void closeEduText();
|
||||
|
||||
void closePip();
|
||||
}
|
||||
|
||||
private void grantPipMenuFocus(boolean grantFocus) {
|
||||
|
||||
@@ -25,119 +25,102 @@ import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
import static android.view.KeyEvent.KEYCODE_DPAD_UP;
|
||||
import static android.view.KeyEvent.KEYCODE_ENTER;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.RemoteAction;
|
||||
import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Handler;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewRootImpl;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.internal.protolog.common.ProtoLog;
|
||||
import com.android.internal.widget.LinearLayoutManager;
|
||||
import com.android.internal.widget.RecyclerView;
|
||||
import com.android.wm.shell.R;
|
||||
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 implements View.OnClickListener {
|
||||
public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.Listener {
|
||||
private static final String TAG = "TvPipMenuView";
|
||||
|
||||
private static final int FIRST_CUSTOM_ACTION_POSITION = 3;
|
||||
private final TvPipMenuView.Listener mListener;
|
||||
|
||||
private final Listener mListener;
|
||||
private final TvPipActionsProvider mTvPipActionsProvider;
|
||||
|
||||
private final RecyclerView mActionButtonsRecyclerView;
|
||||
private final LinearLayoutManager mButtonLayoutManager;
|
||||
private final RecyclerViewAdapter mRecyclerViewAdapter;
|
||||
|
||||
private final LinearLayout mActionButtonsContainer;
|
||||
private final View mMenuFrameView;
|
||||
private final List<TvWindowMenuActionButton> mAdditionalButtons = new ArrayList<>();
|
||||
private final View mPipFrameView;
|
||||
private final View mMenuFrameView;
|
||||
private final View mPipView;
|
||||
|
||||
private final View mPipBackground;
|
||||
private final View mDimLayer;
|
||||
|
||||
private final TvPipMenuEduTextDrawer mEduTextDrawer;
|
||||
|
||||
private final int mPipMenuOuterSpace;
|
||||
private final int mPipMenuBorderWidth;
|
||||
|
||||
private final int mPipMenuFadeAnimationDuration;
|
||||
private final int mResizeAnimationDuration;
|
||||
|
||||
private final ImageView mArrowUp;
|
||||
private final ImageView mArrowRight;
|
||||
private final ImageView mArrowDown;
|
||||
private final ImageView mArrowLeft;
|
||||
private final TvWindowMenuActionButton mA11yDoneButton;
|
||||
|
||||
private final View mPipBackground;
|
||||
private final View mDimLayer;
|
||||
|
||||
private final ScrollView mScrollView;
|
||||
private final HorizontalScrollView mHorizontalScrollView;
|
||||
private View mFocusedButton;
|
||||
|
||||
private Rect mCurrentPipBounds;
|
||||
private boolean mMoveMenuIsVisible;
|
||||
private boolean mButtonMenuIsVisible;
|
||||
|
||||
private final TvWindowMenuActionButton mExpandButton;
|
||||
private final TvWindowMenuActionButton mCloseButton;
|
||||
|
||||
private boolean mSwitchingOrientation;
|
||||
|
||||
private final int mPipMenuFadeAnimationDuration;
|
||||
private final int mResizeAnimationDuration;
|
||||
|
||||
private final AccessibilityManager mA11yManager;
|
||||
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);
|
||||
|
||||
mMainHandler = mainHandler;
|
||||
mListener = listener;
|
||||
|
||||
mA11yManager = context.getSystemService(AccessibilityManager.class);
|
||||
|
||||
mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons);
|
||||
mActionButtonsContainer.findViewById(R.id.tv_pip_menu_fullscreen_button)
|
||||
.setOnClickListener(this);
|
||||
mActionButtonsRecyclerView = findViewById(R.id.tv_pip_menu_action_buttons);
|
||||
mButtonLayoutManager = new LinearLayoutManager(mContext);
|
||||
mActionButtonsRecyclerView.setLayoutManager(mButtonLayoutManager);
|
||||
mActionButtonsRecyclerView.setPreserveFocusAfterLayout(true);
|
||||
|
||||
mCloseButton = mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button);
|
||||
mCloseButton.setOnClickListener(this);
|
||||
mCloseButton.setIsCustomCloseAction(true);
|
||||
mTvPipActionsProvider = tvPipActionsProvider;
|
||||
mRecyclerViewAdapter = new RecyclerViewAdapter(tvPipActionsProvider.getActionsList());
|
||||
mActionButtonsRecyclerView.setAdapter(mRecyclerViewAdapter);
|
||||
|
||||
mActionButtonsContainer.findViewById(R.id.tv_pip_menu_move_button)
|
||||
.setOnClickListener(this);
|
||||
mExpandButton = findViewById(R.id.tv_pip_menu_expand_button);
|
||||
mExpandButton.setOnClickListener(this);
|
||||
|
||||
mPipBackground = findViewById(R.id.tv_pip_menu_background);
|
||||
mDimLayer = findViewById(R.id.tv_pip_menu_dim_layer);
|
||||
|
||||
mScrollView = findViewById(R.id.tv_pip_menu_scroll);
|
||||
mHorizontalScrollView = findViewById(R.id.tv_pip_menu_horizontal_scroll);
|
||||
tvPipActionsProvider.addListener(this);
|
||||
|
||||
mMenuFrameView = findViewById(R.id.tv_pip_menu_frame);
|
||||
mPipFrameView = findViewById(R.id.tv_pip_border);
|
||||
mPipView = findViewById(R.id.tv_pip);
|
||||
|
||||
mPipBackground = findViewById(R.id.tv_pip_menu_background);
|
||||
mDimLayer = findViewById(R.id.tv_pip_menu_dim_layer);
|
||||
|
||||
mArrowUp = findViewById(R.id.tv_pip_menu_arrow_up);
|
||||
mArrowRight = findViewById(R.id.tv_pip_menu_arrow_right);
|
||||
mArrowDown = findViewById(R.id.tv_pip_menu_arrow_down);
|
||||
@@ -160,8 +143,12 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
}
|
||||
|
||||
void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
|
||||
if (targetBounds == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fade out content by fading in view on top.
|
||||
if (mCurrentPipBounds != null && targetBounds != null) {
|
||||
if (mCurrentPipBounds != null) {
|
||||
boolean ratioChanged = PipUtils.aspectRatioChanged(
|
||||
mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(),
|
||||
targetBounds.width() / (float) targetBounds.height());
|
||||
@@ -177,7 +164,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
// Update buttons.
|
||||
final boolean vertical = targetBounds.height() > targetBounds.width();
|
||||
final boolean orientationChanged =
|
||||
vertical != (mActionButtonsContainer.getOrientation() == LinearLayout.VERTICAL);
|
||||
vertical != (mButtonLayoutManager.getOrientation() == LinearLayoutManager.VERTICAL);
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: onPipTransitionToTargetBoundsStarted(), orientation changed %b",
|
||||
TAG, orientationChanged);
|
||||
@@ -187,27 +174,27 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
|
||||
if (mButtonMenuIsVisible) {
|
||||
mSwitchingOrientation = true;
|
||||
mActionButtonsContainer.animate()
|
||||
mActionButtonsRecyclerView.animate()
|
||||
.alpha(0)
|
||||
.setInterpolator(TvPipInterpolators.EXIT)
|
||||
.setDuration(mResizeAnimationDuration / 2)
|
||||
.withEndAction(() -> {
|
||||
changeButtonScrollOrientation(targetBounds);
|
||||
updateButtonGravity(targetBounds);
|
||||
mButtonLayoutManager.setOrientation(vertical
|
||||
? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL);
|
||||
// Only make buttons visible again in onPipTransitionFinished to keep in
|
||||
// sync with PiP content alpha animation.
|
||||
});
|
||||
} else {
|
||||
changeButtonScrollOrientation(targetBounds);
|
||||
updateButtonGravity(targetBounds);
|
||||
mButtonLayoutManager.setOrientation(vertical
|
||||
? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL);
|
||||
}
|
||||
}
|
||||
|
||||
void onPipTransitionFinished(boolean enterTransition, boolean isTvPipExpanded) {
|
||||
void onPipTransitionFinished(boolean enterTransition) {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: onPipTransitionFinished()", TAG);
|
||||
|
||||
// Fade in content by fading out view on top.
|
||||
// Fade in content by fading out view on top (faded out at every aspect ratio change).
|
||||
mPipBackground.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(mResizeAnimationDuration / 2)
|
||||
@@ -218,16 +205,11 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
mEduTextDrawer.init();
|
||||
}
|
||||
|
||||
setIsExpanded(isTvPipExpanded);
|
||||
|
||||
// Update buttons.
|
||||
if (mSwitchingOrientation) {
|
||||
mActionButtonsContainer.animate()
|
||||
mActionButtonsRecyclerView.animate()
|
||||
.alpha(1)
|
||||
.setInterpolator(TvPipInterpolators.ENTER)
|
||||
.setDuration(mResizeAnimationDuration / 2);
|
||||
} else {
|
||||
refocusPreviousButton();
|
||||
}
|
||||
mSwitchingOrientation = false;
|
||||
}
|
||||
@@ -240,107 +222,9 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
"%s: updateLayout, width: %s, height: %s", TAG, updatedBounds.width(),
|
||||
updatedBounds.height());
|
||||
mCurrentPipBounds = updatedBounds;
|
||||
if (!mSwitchingOrientation) {
|
||||
updateButtonGravity(mCurrentPipBounds);
|
||||
}
|
||||
|
||||
updatePipFrameBounds();
|
||||
}
|
||||
|
||||
private void changeButtonScrollOrientation(Rect bounds) {
|
||||
final boolean vertical = bounds.height() > bounds.width();
|
||||
|
||||
final ViewGroup oldScrollView = vertical ? mHorizontalScrollView : mScrollView;
|
||||
final ViewGroup newScrollView = vertical ? mScrollView : mHorizontalScrollView;
|
||||
|
||||
if (oldScrollView.getChildCount() == 1) {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: orientation changed", TAG);
|
||||
oldScrollView.removeView(mActionButtonsContainer);
|
||||
oldScrollView.setVisibility(GONE);
|
||||
mActionButtonsContainer.setOrientation(vertical ? LinearLayout.VERTICAL
|
||||
: LinearLayout.HORIZONTAL);
|
||||
newScrollView.addView(mActionButtonsContainer);
|
||||
newScrollView.setVisibility(VISIBLE);
|
||||
if (mFocusedButton != null) {
|
||||
mFocusedButton.requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change button gravity based on new dimensions
|
||||
*/
|
||||
private void updateButtonGravity(Rect bounds) {
|
||||
final boolean vertical = bounds.height() > bounds.width();
|
||||
// Use Math.max since the possible orientation change might not have been applied yet.
|
||||
final int buttonsSize = Math.max(mActionButtonsContainer.getHeight(),
|
||||
mActionButtonsContainer.getWidth());
|
||||
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: buttons container width: %s, height: %s", TAG,
|
||||
mActionButtonsContainer.getWidth(), mActionButtonsContainer.getHeight());
|
||||
|
||||
final boolean buttonsFit =
|
||||
vertical ? buttonsSize < bounds.height()
|
||||
: buttonsSize < bounds.width();
|
||||
final int buttonGravity = buttonsFit ? Gravity.CENTER
|
||||
: (vertical ? Gravity.CENTER_HORIZONTAL : Gravity.CENTER_VERTICAL);
|
||||
|
||||
final LayoutParams params = (LayoutParams) mActionButtonsContainer.getLayoutParams();
|
||||
params.gravity = buttonGravity;
|
||||
mActionButtonsContainer.setLayoutParams(params);
|
||||
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: vertical: %b, buttonsFit: %b, gravity: %s", TAG, vertical, buttonsFit,
|
||||
Gravity.toString(buttonGravity));
|
||||
}
|
||||
|
||||
private void refocusPreviousButton() {
|
||||
if (mMoveMenuIsVisible || mCurrentPipBounds == null || mFocusedButton == null) {
|
||||
return;
|
||||
}
|
||||
final boolean vertical = mCurrentPipBounds.height() > mCurrentPipBounds.width();
|
||||
|
||||
if (!mFocusedButton.hasFocus()) {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: request focus from: %s", TAG, mFocusedButton);
|
||||
mFocusedButton.requestFocus();
|
||||
} else {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: already focused: %s", TAG, mFocusedButton);
|
||||
}
|
||||
|
||||
// Do we need to scroll?
|
||||
final Rect buttonBounds = new Rect();
|
||||
final Rect scrollBounds = new Rect();
|
||||
if (vertical) {
|
||||
mScrollView.getDrawingRect(scrollBounds);
|
||||
} else {
|
||||
mHorizontalScrollView.getDrawingRect(scrollBounds);
|
||||
}
|
||||
mFocusedButton.getHitRect(buttonBounds);
|
||||
|
||||
if (scrollBounds.contains(buttonBounds)) {
|
||||
// Button is already completely visible, don't scroll
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: not scrolling", TAG);
|
||||
return;
|
||||
}
|
||||
|
||||
// Scrolling so the button is visible to the user.
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: scrolling to focused button", TAG);
|
||||
|
||||
if (vertical) {
|
||||
mScrollView.smoothScrollTo((int) mFocusedButton.getX(),
|
||||
(int) mFocusedButton.getY());
|
||||
} else {
|
||||
mHorizontalScrollView.smoothScrollTo((int) mFocusedButton.getX(),
|
||||
(int) mFocusedButton.getY());
|
||||
}
|
||||
}
|
||||
|
||||
Rect getPipMenuContainerBounds(Rect pipBounds) {
|
||||
final Rect menuUiBounds = new Rect(pipBounds);
|
||||
menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace);
|
||||
@@ -370,20 +254,14 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
mPipView.setLayoutParams(pipViewParams);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
void setExpandedModeEnabled(boolean enabled) {
|
||||
mExpandButton.setVisibility(enabled ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
void setIsExpanded(boolean expanded) {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: setIsExpanded, expanded: %b", TAG, expanded);
|
||||
mExpandButton.setImageResource(
|
||||
expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand);
|
||||
mExpandButton.setTextAndDescription(
|
||||
expanded ? R.string.pip_collapse : R.string.pip_expand);
|
||||
// Keep focused button within the visible area while the PiP is changing size. Otherwise,
|
||||
// the button would lose focus which would cause a need for scrolling and re-focusing after
|
||||
// the animation finishes, which does not look good.
|
||||
View focusedChild = mActionButtonsRecyclerView.getFocusedChild();
|
||||
if (focusedChild != null) {
|
||||
mActionButtonsRecyclerView.scrollToPosition(
|
||||
mActionButtonsRecyclerView.getChildLayoutPosition(focusedChild));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -391,48 +269,63 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
*/
|
||||
void showMoveMenu(int gravity) {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG);
|
||||
showButtonsMenu(false);
|
||||
showMovementHints(gravity);
|
||||
setMenuButtonsVisible(false);
|
||||
setFrameHighlighted(true);
|
||||
|
||||
mHorizontalScrollView.setFocusable(false);
|
||||
mScrollView.setFocusable(false);
|
||||
animateAlphaTo(mA11yManager.isEnabled() ? 1f : 0f, mDimLayer);
|
||||
|
||||
mEduTextDrawer.closeIfNeeded();
|
||||
}
|
||||
|
||||
void showButtonsMenu() {
|
||||
|
||||
void showButtonsMenu(boolean exitingMoveMode) {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: showButtonsMenu()", TAG);
|
||||
showButtonsMenu(true);
|
||||
"%s: showButtonsMenu(), exitingMoveMode %b", TAG, exitingMoveMode);
|
||||
setMenuButtonsVisible(true);
|
||||
hideMovementHints();
|
||||
setFrameHighlighted(true);
|
||||
animateAlphaTo(1f, mDimLayer);
|
||||
mEduTextDrawer.closeIfNeeded();
|
||||
|
||||
mHorizontalScrollView.setFocusable(true);
|
||||
mScrollView.setFocusable(true);
|
||||
|
||||
// Always focus on the first button when opening the menu, except directly after moving.
|
||||
if (mFocusedButton == null) {
|
||||
// Focus on first button (there is a Space at position 0)
|
||||
mFocusedButton = mActionButtonsContainer.getChildAt(1);
|
||||
// Reset scroll position.
|
||||
mScrollView.scrollTo(0, 0);
|
||||
mHorizontalScrollView.scrollTo(
|
||||
isLayoutRtl() ? mActionButtonsContainer.getWidth() : 0, 0);
|
||||
if (exitingMoveMode) {
|
||||
scrollAndRefocusButton(mTvPipActionsProvider.getFirstIndexOfAction(ACTION_MOVE),
|
||||
/* alwaysScroll= */ false);
|
||||
} else {
|
||||
scrollAndRefocusButton(0, /* alwaysScroll= */ true);
|
||||
}
|
||||
}
|
||||
|
||||
private void scrollAndRefocusButton(int position, boolean alwaysScroll) {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: scrollAndRefocusButton, target: %d", TAG, position);
|
||||
|
||||
if (alwaysScroll || !refocusButton(position)) {
|
||||
mButtonLayoutManager.scrollToPositionWithOffset(position, 0);
|
||||
mActionButtonsRecyclerView.post(() -> refocusButton(position));
|
||||
}
|
||||
refocusPreviousButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides all menu views, including the menu frame.
|
||||
* @return true if focus was requested, false if focus request could not be carried out due to
|
||||
* the view for the position not being available (scrolling beforehand will be necessary).
|
||||
*/
|
||||
private boolean refocusButton(int position) {
|
||||
View itemToFocus = mButtonLayoutManager.findViewByPosition(position);
|
||||
if (itemToFocus != null) {
|
||||
itemToFocus.requestFocus();
|
||||
itemToFocus.requestAccessibilityFocus();
|
||||
}
|
||||
return itemToFocus != null;
|
||||
}
|
||||
|
||||
void hideAllUserControls() {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: hideAllUserControls()", TAG);
|
||||
mFocusedButton = null;
|
||||
showButtonsMenu(false);
|
||||
setMenuButtonsVisible(false);
|
||||
hideMovementHints();
|
||||
setFrameHighlighted(false);
|
||||
animateAlphaTo(0f, mDimLayer);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -463,134 +356,19 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Button order:
|
||||
* - Fullscreen
|
||||
* - Close
|
||||
* - Custom actions (app or media actions)
|
||||
* - System actions
|
||||
*/
|
||||
void setAdditionalActions(List<RemoteAction> actions, RemoteAction closeAction,
|
||||
Handler mainHandler) {
|
||||
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();
|
||||
if (actionsNumber > buttonsNumber) {
|
||||
// Add buttons until we have enough to display all the actions.
|
||||
while (actionsNumber > buttonsNumber) {
|
||||
TvWindowMenuActionButton button = new TvWindowMenuActionButton(mContext);
|
||||
button.setOnClickListener(this);
|
||||
|
||||
mActionButtonsContainer.addView(button,
|
||||
FIRST_CUSTOM_ACTION_POSITION + buttonsNumber);
|
||||
mAdditionalButtons.add(button);
|
||||
|
||||
buttonsNumber++;
|
||||
}
|
||||
} else if (actionsNumber < buttonsNumber) {
|
||||
// Hide buttons until we as many as the actions.
|
||||
while (actionsNumber < buttonsNumber) {
|
||||
final View button = mAdditionalButtons.get(buttonsNumber - 1);
|
||||
button.setVisibility(View.GONE);
|
||||
button.setTag(null);
|
||||
|
||||
buttonsNumber--;
|
||||
}
|
||||
}
|
||||
|
||||
// "Assign" actions to the buttons.
|
||||
for (int index = 0; index < actionsNumber; index++) {
|
||||
final RemoteAction action = actions.get(index);
|
||||
final TvWindowMenuActionButton button = mAdditionalButtons.get(index);
|
||||
|
||||
// Remove action if it matches the custom close action.
|
||||
if (PipUtils.remoteActionsMatch(action, closeAction)) {
|
||||
button.setVisibility(GONE);
|
||||
continue;
|
||||
}
|
||||
setActionForButton(action, button, mainHandler);
|
||||
}
|
||||
|
||||
if (mCurrentPipBounds != null) {
|
||||
updateButtonGravity(mCurrentPipBounds);
|
||||
refocusPreviousButton();
|
||||
}
|
||||
}
|
||||
|
||||
private void setActionForButton(RemoteAction action, TvWindowMenuActionButton button,
|
||||
Handler mainHandler) {
|
||||
button.setVisibility(View.VISIBLE); // Ensure the button is visible.
|
||||
if (action.getContentDescription().length() > 0) {
|
||||
button.setTextAndDescription(action.getContentDescription());
|
||||
} else {
|
||||
button.setTextAndDescription(action.getTitle());
|
||||
}
|
||||
button.setEnabled(action.isEnabled());
|
||||
button.setTag(action);
|
||||
action.getIcon().loadDrawableAsync(mContext, button::setImageDrawable, mainHandler);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
SurfaceControl getWindowSurfaceControl() {
|
||||
final ViewRootImpl root = getViewRootImpl();
|
||||
if (root == null) {
|
||||
return null;
|
||||
}
|
||||
final SurfaceControl out = root.getSurfaceControl();
|
||||
if (out != null && out.isValid()) {
|
||||
return out;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final int id = v.getId();
|
||||
if (id == R.id.tv_pip_menu_fullscreen_button) {
|
||||
mListener.onFullscreenButtonClick();
|
||||
} else if (id == R.id.tv_pip_menu_move_button) {
|
||||
mListener.onEnterMoveMode();
|
||||
} else if (id == R.id.tv_pip_menu_close_button) {
|
||||
mListener.onCloseButtonClick();
|
||||
} else if (id == R.id.tv_pip_menu_expand_button) {
|
||||
mListener.onToggleExpandedMode();
|
||||
} else {
|
||||
// This should be an "additional action"
|
||||
final RemoteAction action = (RemoteAction) v.getTag();
|
||||
if (action != null) {
|
||||
try {
|
||||
action.getActionIntent().send();
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: Failed to send action, %s", TAG, e);
|
||||
}
|
||||
} else {
|
||||
ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: RemoteAction is null", TAG);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (event.getAction() == ACTION_UP) {
|
||||
if (!mMoveMenuIsVisible) {
|
||||
mFocusedButton = mActionButtonsContainer.getFocusedChild();
|
||||
}
|
||||
|
||||
if (event.getKeyCode() == KEYCODE_BACK) {
|
||||
mListener.onBackPress();
|
||||
@@ -624,10 +402,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
public void showMovementHints(int gravity) {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity));
|
||||
|
||||
if (mMoveMenuIsVisible) {
|
||||
return;
|
||||
}
|
||||
mMoveMenuIsVisible = true;
|
||||
|
||||
animateAlphaTo(checkGravity(gravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp);
|
||||
@@ -643,9 +417,12 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
|
||||
animateAlphaTo(a11yEnabled ? 1f : 0f, mA11yDoneButton);
|
||||
if (a11yEnabled) {
|
||||
mA11yDoneButton.setVisibility(VISIBLE);
|
||||
mA11yDoneButton.setOnClickListener(v -> {
|
||||
mListener.onExitMoveMode();
|
||||
});
|
||||
mA11yDoneButton.requestFocus();
|
||||
mA11yDoneButton.requestAccessibilityFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,33 +461,67 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
/**
|
||||
* Show or hide the pip buttons menu.
|
||||
*/
|
||||
public void showButtonsMenu(boolean show) {
|
||||
private void setMenuButtonsVisible(boolean visible) {
|
||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||
"%s: showUserActions: %b", TAG, show);
|
||||
if (mButtonMenuIsVisible == show) {
|
||||
return;
|
||||
}
|
||||
mButtonMenuIsVisible = show;
|
||||
|
||||
if (show) {
|
||||
mActionButtonsContainer.setVisibility(VISIBLE);
|
||||
refocusPreviousButton();
|
||||
}
|
||||
animateAlphaTo(show ? 1 : 0, mActionButtonsContainer);
|
||||
animateAlphaTo(show ? 1 : 0, mDimLayer);
|
||||
mEduTextDrawer.closeIfNeeded();
|
||||
"%s: showUserActions: %b", TAG, visible);
|
||||
mButtonMenuIsVisible = visible;
|
||||
animateAlphaTo(visible ? 1 : 0, mActionButtonsRecyclerView);
|
||||
}
|
||||
|
||||
private void setFrameHighlighted(boolean highlighted) {
|
||||
mMenuFrameView.setActivated(highlighted);
|
||||
}
|
||||
|
||||
private class RecyclerViewAdapter extends
|
||||
RecyclerView.Adapter<RecyclerViewAdapter.ButtonViewHolder> {
|
||||
|
||||
private final List<TvPipAction> mActionList;
|
||||
|
||||
RecyclerViewAdapter(List<TvPipAction> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
* <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 {
|
||||
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<RemoteAction> mCustomActions = new ArrayList<>();
|
||||
private final List<RemoteAction> 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<RemoteAction> 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<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)));
|
||||
.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<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();
|
||||
}
|
||||
|
||||
interface Delegate {
|
||||
void showPictureInPictureMenu();
|
||||
|
||||
void closePip();
|
||||
|
||||
void enterPipMovementMenu();
|
||||
|
||||
void togglePipExpansion();
|
||||
|
||||
void movePipToFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<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,
|
||||
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<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}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user