Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 63a37a3a authored by Sergey Nikolaienkov's avatar Sergey Nikolaienkov
Browse files

Use PipMediaController in .tv.PipController

Make .tv.PipController rely on PipMediaController for media session
monitoring and managering.
Refactor .tv.PipControlsViewController to use PipMediaController to
retrieve the media buttons.
Make .tv.PipNotification subscribe to PipMediaController to receive
callbacks about media metadata updates.

Bug: 169575409
Bug: 165795012
Test: make WindowManager-Shell; make SystemUI
Change-Id: I397f93eb4433116ba4f476bda7374b07d97e082d
parent c18512fe
Loading
Loading
Loading
Loading
+0 −10
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@
-->
<!-- Layout for {@link com.android.wm.shell.pip.tv.PipControlsView}. -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <com.android.wm.shell.pip.tv.PipControlButtonView
        android:id="@+id/full_button"
        android:layout_width="@dimen/picture_in_picture_button_width"
@@ -31,13 +30,4 @@
        android:layout_marginStart="@dimen/picture_in_picture_button_start_margin"
        android:src="@drawable/pip_ic_close_white"
        android:text="@string/pip_close" />

    <com.android.wm.shell.pip.tv.PipControlButtonView
        android:id="@+id/play_pause_button"
        android:layout_width="@dimen/picture_in_picture_button_width"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/picture_in_picture_button_start_margin"
        android:src="@drawable/pip_ic_pause_white"
        android:text="@string/pip_pause"
        android:visibility="gone" />
</merge>
+0 −13
Original line number Diff line number Diff line
@@ -24,7 +24,6 @@ import android.content.pm.ActivityInfo;
import android.graphics.Rect;

import com.android.wm.shell.pip.phone.PipTouchHandler;
import com.android.wm.shell.pip.tv.PipController;

import java.io.PrintWriter;
import java.util.function.Consumer;
@@ -33,12 +32,6 @@ import java.util.function.Consumer;
 * Interface to engage picture in picture feature.
 */
public interface Pip {
    /**
     * Registers a {@link PipController.MediaListener} to PipController.
     */
    default void addMediaListener(PipController.MediaListener listener) {
    }

    /**
     * Closes PIP (PIPed activity and PIP system UI).
     */
@@ -144,12 +137,6 @@ public interface Pip {
    default void onTaskStackChanged() {
    }

    /**
     * Removes a {@link PipController.MediaListener} from PipController.
     */
    default void removeMediaListener(PipController.MediaListener listener) {
    }

    /**
     * Resize the Pip to the appropriate size for the input state.
     *
+10 −104
Original line number Diff line number Diff line
@@ -38,9 +38,6 @@ import android.content.pm.ParceledListSlice;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import android.os.Debug;
import android.os.Handler;
import android.os.RemoteException;
@@ -55,6 +52,7 @@ import com.android.wm.shell.pip.PinnedStackListenerForwarder;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.pip.PipBoundsHandler;
import com.android.wm.shell.pip.PipBoundsState;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipTaskOrganizer;

import java.util.ArrayList;
@@ -110,22 +108,19 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
    private final PipBoundsState mPipBoundsState;
    private final PipBoundsHandler mPipBoundsHandler;
    private final PipTaskOrganizer mPipTaskOrganizer;
    private final PipMediaController mPipMediaController;

    private IActivityTaskManager mActivityTaskManager;
    private MediaSessionManager mMediaSessionManager;
    private int mState = STATE_NO_PIP;
    private int mResumeResizePinnedStackRunnableState = STATE_NO_PIP;
    private final Handler mHandler = new Handler();
    private List<Listener> mListeners = new ArrayList<>();
    private List<MediaListener> mMediaListeners = new ArrayList<>();
    private Rect mPipBounds;
    private Rect mDefaultPipBounds = new Rect();
    private Rect mMenuModePipBounds;
    private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED;
    private int mPipTaskId = TASK_ID_NO_PIP;
    private int mPinnedStackId = INVALID_STACK_ID;
    private ComponentName mPipComponentName;
    private MediaController mPipMediaController;
    private String[] mLastPackagesResourceGranted;
    private PipNotification mPipNotification;
    private ParceledListSlice<RemoteAction> mCustomActions;
@@ -168,17 +163,13 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
            }
        }
    };
    private final MediaSessionManager.OnActiveSessionsChangedListener mActiveMediaSessionListener =
            controllers -> updateMediaController(controllers);

    private final PinnedStackListenerForwarder.PinnedStackListener mPinnedStackListener =
            new PipControllerPinnedStackListener();

    @Override
    public void registerSessionListenerForCurrentUser() {
        // TODO Need confirm if TV have to re-registers when switch user
        mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveMediaSessionListener);
        mMediaSessionManager.addOnActiveSessionsChangedListener(mActiveMediaSessionListener, null,
                UserHandle.USER_CURRENT, null);
        mPipMediaController.registerSessionListenerForCurrentUser();
    }

    /**
@@ -232,12 +223,13 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
            PipBoundsState pipBoundsState,
            PipBoundsHandler pipBoundsHandler,
            PipTaskOrganizer pipTaskOrganizer,
            WindowManagerShellWrapper windowManagerShellWrapper
    ) {
            PipMediaController pipMediaController,
            WindowManagerShellWrapper windowManagerShellWrapper) {
        mContext = context;
        mPipBoundsState = pipBoundsState;
        mPipNotification = new PipNotification(context, this);
        mPipBoundsHandler = pipBoundsHandler;
        mPipMediaController = pipMediaController;
        // Ensure that we have the display info in case we get calls to update the bounds
        // before the listener calls back
        final DisplayInfo displayInfo = new DisplayInfo();
@@ -261,7 +253,6 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
        mLastOrientation = initialConfig.orientation;
        loadConfigurationsAndApply(initialConfig);

        mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class);
        mWindowManagerShellWrapper = windowManagerShellWrapper;
        try {
            mWindowManagerShellWrapper.addPinnedStackListener(mPinnedStackListener);
@@ -329,8 +320,6 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac

        mState = STATE_NO_PIP;
        mPipTaskId = TASK_ID_NO_PIP;
        mPipMediaController = null;
        mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveMediaSessionListener);
        if (removePipStack) {
            try {
                mActivityTaskManager.removeTask(mPinnedStackId);
@@ -371,13 +360,9 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
        if (DEBUG) Log.d(TAG, "PINNED_STACK:" + taskInfo);
        mPinnedStackId = taskInfo.taskId;
        mPipTaskId = taskInfo.childTaskIds[taskInfo.childTaskIds.length - 1];
        mPipComponentName = ComponentName.unflattenFromString(
                taskInfo.childTaskNames[taskInfo.childTaskNames.length - 1]);
        // Set state to STATE_PIP so we show it when the pinned stack animation ends.
        mState = STATE_PIP;
        mMediaSessionManager.addOnActiveSessionsChangedListener(
                mActiveMediaSessionListener, null);
        updateMediaController(mMediaSessionManager.getActiveSessions(null));
        mPipMediaController.onActivityPinned();
        for (int i = mListeners.size() - 1; i >= 0; i--) {
            mListeners.get(i).onPipEntered(packageName);
        }
@@ -553,20 +538,6 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
        mListeners.remove(listener);
    }

    /**
     * Adds a {@link MediaListener} to PipController.
     */
    public void addMediaListener(MediaListener listener) {
        mMediaListeners.add(listener);
    }

    /**
     * Removes a {@link MediaListener} from PipController.
     */
    public void removeMediaListener(MediaListener listener) {
        mMediaListeners.remove(listener);
    }

    /**
     * Returns {@code true} if PIP is shown.
     */
@@ -608,69 +579,12 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
        }
    }

    private void updateMediaController(List<MediaController> controllers) {
        MediaController mediaController = null;
        if (controllers != null && getState() != STATE_NO_PIP && mPipComponentName != null) {
            for (int i = controllers.size() - 1; i >= 0; i--) {
                MediaController controller = controllers.get(i);
                // We assumes that an app with PIPable activity
                // keeps the single instance of media controller especially when PIP is on.
                if (controller.getPackageName().equals(mPipComponentName.getPackageName())) {
                    mediaController = controller;
                    break;
                }
            }
        }
        if (mPipMediaController != mediaController) {
            mPipMediaController = mediaController;
            for (int i = mMediaListeners.size() - 1; i >= 0; i--) {
                mMediaListeners.get(i).onMediaControllerChanged();
            }
            if (mPipMediaController == null) {
                mHandler.postDelayed(mClosePipRunnable,
                        CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS);
            } else {
                mHandler.removeCallbacks(mClosePipRunnable);
            }
        }
    }

    /**
     * Gets the {@link android.media.session.MediaController} for the PIPed activity.
     */
    MediaController getMediaController() {
        return mPipMediaController;
    }

    @Override
    public void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) {

    }

    /**
     * Returns the PIPed activity's playback state.
     * This returns one of {@link #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED},
     * or {@link #PLAYBACK_STATE_UNAVAILABLE}.
     */
    int getPlaybackState() {
        if (mPipMediaController == null || mPipMediaController.getPlaybackState() == null) {
            return PLAYBACK_STATE_UNAVAILABLE;
        }
        int state = mPipMediaController.getPlaybackState().getState();
        boolean isPlaying = (state == PlaybackState.STATE_BUFFERING
                || state == PlaybackState.STATE_CONNECTING
                || state == PlaybackState.STATE_PLAYING
                || state == PlaybackState.STATE_FAST_FORWARDING
                || state == PlaybackState.STATE_REWINDING
                || state == PlaybackState.STATE_SKIPPING_TO_PREVIOUS
                || state == PlaybackState.STATE_SKIPPING_TO_NEXT);
        long actions = mPipMediaController.getPlaybackState().getActions();
        if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) {
            return PLAYBACK_STATE_PAUSED;
        } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) {
            return PLAYBACK_STATE_PLAYING;
        }
        return PLAYBACK_STATE_UNAVAILABLE;
    PipMediaController getPipMediaController() {
        return mPipMediaController;
    }

    @Override
@@ -718,14 +632,6 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
        void onPipResizeAboutToStart();
    }

    /**
     * A listener interface to receive change in PIP's media controller
     */
    public interface MediaListener {
        /** Invoked when the MediaController on PIPed activity is changed. */
        void onMediaControllerChanged();
    }

    private String getStateDescription() {
        if (mSuspendPipResizingReason == 0) {
            return stateToName(mState);
+2 −6
Original line number Diff line number Diff line
@@ -51,15 +51,11 @@ public class PipControlsView extends LinearLayout {
        setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
    }

    PipControlButtonView getFullButtonView() {
    PipControlButtonView getFullscreenButton() {
        return findViewById(R.id.full_button);
    }

    PipControlButtonView getCloseButtonView() {
    PipControlButtonView getCloseButton() {
        return findViewById(R.id.close_button);
    }

    PipControlButtonView getPlayPauseButtonView() {
        return findViewById(R.id.play_pause_button);
    }
}
+91 −187
Original line number Diff line number Diff line
@@ -18,10 +18,10 @@ package com.android.wm.shell.pip.tv;

import android.app.PendingIntent;
import android.app.RemoteAction;
import android.content.Context;
import android.graphics.Color;
import android.media.session.MediaController;
import android.media.session.PlaybackState;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -29,9 +29,8 @@ import android.view.View;
import com.android.wm.shell.R;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;


/**
@@ -42,213 +41,118 @@ public class PipControlsViewController {

    private static final float DISABLED_ACTION_ALPHA = 0.54f;

    private final PipControlsView mView;
    private final LayoutInflater mLayoutInflater;
    private final Handler mHandler;
    private final PipController mPipController;
    private final PipControlButtonView mPlayPauseButtonView;
    private MediaController mMediaController;
    private PipControlButtonView mFocusedChild;
    private Listener mListener;
    private ArrayList<PipControlButtonView> mCustomButtonViews = new ArrayList<>();
    private List<RemoteAction> mCustomActions = new ArrayList<>();

    public PipControlsView getView() {
        return mView;
    }

    /**
     * An interface to listen user action.
     */
    public interface Listener {
        /**
         * Called when a user clicks close PIP button.
         */
        void onClosed();
    }

    private View.OnAttachStateChangeListener
            mOnAttachStateChangeListener =
            new View.OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    updateMediaController();
                    mPipController.addMediaListener(mPipMediaListener);
                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                    mPipController.removeMediaListener(mPipMediaListener);
                }
            };
    private final Context mContext;
    private final Handler mUiThreadHandler;
    private final PipControlsView mView;
    private final List<PipControlButtonView> mAdditionalButtons = new ArrayList<>();

    private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() {
        @Override
        public void onPlaybackStateChanged(PlaybackState state) {
            updateUserActions();
        }
    };

    private final PipController.MediaListener mPipMediaListener = this::updateMediaController;

    private final View.OnFocusChangeListener
            mFocusChangeListener =
            new View.OnFocusChangeListener() {
                @Override
                public void onFocusChange(View view, boolean hasFocus) {
                    if (hasFocus) {
                        mFocusedChild = (PipControlButtonView) view;
                    } else if (mFocusedChild == view) {
                        mFocusedChild = null;
                    }
                }
            };
    private final List<RemoteAction> mCustomActions = new ArrayList<>();
    private final List<RemoteAction> mMediaActions = new ArrayList<>();

    public PipControlsViewController(PipControlsView view, PipController pipController,
            LayoutInflater layoutInflater, Handler handler) {
        super();
        mView = view;
    public PipControlsViewController(PipControlsView view, PipController pipController) {
        mContext = view.getContext();
        mUiThreadHandler = new Handler(Looper.getMainLooper());
        mPipController = pipController;
        mLayoutInflater = layoutInflater;
        mHandler = handler;
        mView = view;

        mView.addOnAttachStateChangeListener(mOnAttachStateChangeListener);
        if (mView.isAttachedToWindow()) {
            mOnAttachStateChangeListener.onViewAttachedToWindow(mView);
        }
        mView.getFullscreenButton().setOnClickListener(v -> mPipController.movePipToFullscreen());
        mView.getCloseButton().setOnClickListener(v -> mPipController.closePip());

        View fullButtonView = mView.getFullButtonView();
        fullButtonView.setOnFocusChangeListener(mFocusChangeListener);
        fullButtonView.setOnClickListener(mView -> mPipController.movePipToFullscreen());
        mPipController.getPipMediaController().addActionListener(this::onMediaActionsChanged);
    }

        View closeButtonView = mView.getCloseButtonView();
        closeButtonView.setOnFocusChangeListener(mFocusChangeListener);
        closeButtonView.setOnClickListener(v -> {
            mPipController.closePip();
            if (mListener != null) {
                mListener.onClosed();
    PipControlsView getView() {
        return mView;
    }
        });

        mPlayPauseButtonView = mView.getPlayPauseButtonView();
        mPlayPauseButtonView.setOnFocusChangeListener(mFocusChangeListener);
        mPlayPauseButtonView.setOnClickListener(v -> {
            if (mMediaController == null || mMediaController.getPlaybackState() == null) {
    /**
     * Updates the set of activity-defined actions.
     */
    void setCustomActions(List<? extends RemoteAction> actions) {
        if (mCustomActions.isEmpty() && actions.isEmpty()) {
            // Nothing changed - return early.
            return;
        }
            final int playbackState = mPipController.getPlaybackState();
            if (playbackState == PipController.PLAYBACK_STATE_PAUSED) {
                mMediaController.getTransportControls().play();
            } else if (playbackState == PipController.PLAYBACK_STATE_PLAYING) {
                mMediaController.getTransportControls().pause();
            }

            // View will be updated later in {@link mMediaControllerCallback}
        });
        mCustomActions.clear();
        mCustomActions.addAll(actions);
        updateAdditionalActions();
    }

    private void updateMediaController() {
        AtomicReference<MediaController> newController = new AtomicReference<>();
        newController.set(mPipController.getMediaController());

        if (newController.get() == null || mMediaController == newController.get()) {
    private void onMediaActionsChanged(List<RemoteAction> actions) {
        if (mMediaActions.isEmpty() && actions.isEmpty()) {
            // Nothing changed - return early.
            return;
        }
        if (mMediaController != null) {
            mMediaController.unregisterCallback(mMediaControllerCallback);
        }
        mMediaController = newController.get();
        if (mMediaController != null) {
            mMediaController.registerCallback(mMediaControllerCallback);
        mMediaActions.clear();
        mMediaActions.addAll(actions);

        // Update the view only if there are no custom actions (media actions are only shown when
        // there no custom actions).
        if (mCustomActions.isEmpty()) {
            updateAdditionalActions();
        }
        updateUserActions();
    }

    /**
     * Updates the actions for the PIP. If there are no custom actions, then the media session
     * actions are shown.
     */
    private void updateUserActions() {
    private void updateAdditionalActions() {
        final List<RemoteAction> actionsToDisplay;
        if (!mCustomActions.isEmpty()) {
            // Ensure we have as many buttons as actions
            while (mCustomButtonViews.size() < mCustomActions.size()) {
                PipControlButtonView buttonView = (PipControlButtonView) mLayoutInflater.inflate(
            // If there are custom actions: show them.
            actionsToDisplay = mCustomActions;
        } else if (!mMediaActions.isEmpty()) {
            // If there are no custom actions, but there media actions: show them.
            actionsToDisplay = mMediaActions;
        } else {
            // If there no custom actions and no media actions: clean up all the additional buttons.
            actionsToDisplay = Collections.emptyList();
        }

        // Make sure we exactly as many additional buttons as we have actions to display.
        final int actionsNumber = actionsToDisplay.size();
        int buttonsNumber = mAdditionalButtons.size();
        if (actionsNumber > buttonsNumber) {
            final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
            // Add buttons until we have enough to display all of the actions.
            while (actionsNumber > buttonsNumber) {
                final PipControlButtonView button = (PipControlButtonView) layoutInflater.inflate(
                        R.layout.tv_pip_custom_control, mView, false);
                mView.addView(buttonView);
                mCustomButtonViews.add(buttonView);
                mView.addView(button);
                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.setOnClickListener(null);

            // Update the visibility of all views
            for (int i = 0; i < mCustomButtonViews.size(); i++) {
                mCustomButtonViews.get(i).setVisibility(
                        i < mCustomActions.size() ? View.VISIBLE : View.GONE);
                buttonsNumber--;
            }
        }

            // Update the state and visibility of the action buttons, and hide the rest
            for (int i = 0; i < mCustomActions.size(); i++) {
                final RemoteAction action = mCustomActions.get(i);
                PipControlButtonView actionView = mCustomButtonViews.get(i);

                // TODO: Check if the action drawable has changed before we reload it
                action.getIcon().loadDrawableAsync(mView.getContext(), d -> {
                    d.setTint(Color.WHITE);
                    actionView.setImageDrawable(d);
                }, mHandler);
                actionView.setText(action.getContentDescription());
                if (action.isEnabled()) {
                    actionView.setOnClickListener(v -> {
        // "Assign" actions to the buttons.
        for (int index = 0; index < actionsNumber; index++) {
            final RemoteAction action = actionsToDisplay.get(index);
            final PipControlButtonView button = mAdditionalButtons.get(index);
            button.setVisibility(View.VISIBLE); // Ensure the button is visible.
            button.setText(action.getContentDescription());
            button.setEnabled(action.isEnabled());
            button.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA);
            button.setOnClickListener(v -> {
                try {
                    action.getActionIntent().send();
                } catch (PendingIntent.CanceledException e) {
                    Log.w(TAG, "Failed to send action", e);
                }
            });
                }
                actionView.setEnabled(action.isEnabled());
                actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA);
            }

            // Hide the media session buttons
            mPlayPauseButtonView.setVisibility(View.GONE);
        } else {
            AtomicInteger state = new AtomicInteger(PipController.STATE_UNKNOWN);
            state.set(mPipController.getPlaybackState());
            if (state.get() == PipController.STATE_UNKNOWN
                    || state.get() == PipController.PLAYBACK_STATE_UNAVAILABLE) {
                mPlayPauseButtonView.setVisibility(View.GONE);
            } else {
                mPlayPauseButtonView.setVisibility(View.VISIBLE);
                if (state.get() == PipController.PLAYBACK_STATE_PLAYING) {
                    mPlayPauseButtonView.setImageResource(R.drawable.pip_ic_pause_white);
                    mPlayPauseButtonView.setText(R.string.pip_pause);
                } else {
                    mPlayPauseButtonView.setImageResource(R.drawable.pip_ic_play_arrow_white);
                    mPlayPauseButtonView.setText(R.string.pip_play);
                }
            }

            // Hide all the custom action buttons
            for (int i = 0; i < mCustomButtonViews.size(); i++) {
                mCustomButtonViews.get(i).setVisibility(View.GONE);
            }
        }
    }


    /**
     * Sets the {@link Listener} to listen user actions.
     */
    public void setListener(Listener listener) {
        mListener = listener;
            action.getIcon().loadDrawableAsync(mContext, drawable -> {
                drawable.setTint(Color.WHITE);
                button.setImageDrawable(drawable);
            }, mUiThreadHandler);
        }


    /**
     * Updates the set of activity-defined actions.
     */
    public void setActions(List<? extends RemoteAction> actions) {
        mCustomActions.clear();
        mCustomActions.addAll(actions);
        updateUserActions();
    }
}
Loading