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

Commit 97a60d9d authored by Winson Chung's avatar Winson Chung
Browse files

Falling back to media session controls.

- If there are no specific actions set by the current PiP activity,
  then fall back to actions provided by the current media session.

Test: Start PiP activity with a media session

Change-Id: Iabb33606eec682fa2fa4d1ba065c8b2763023de7
parent 3535df26
Loading
Loading
Loading
Loading
+9 −13
Original line number Diff line number Diff line
@@ -55,6 +55,7 @@ public class PipManager {
    private final PinnedStackListener mPinnedStackListener = new PinnedStackListener();

    private PipMenuActivityController mMenuController;
    private PipMediaController mMediaController;
    private PipTouchHandler mTouchHandler;

    /**
@@ -67,6 +68,7 @@ public class PipManager {
                return;
            }
            mTouchHandler.onActivityPinned();
            mMediaController.onActivityPinned();
        }

        @Override
@@ -84,18 +86,10 @@ public class PipManager {
            // another package than the top activity in the stack
            boolean expandPipToFullscreen = true;
            if (sourceComponent != null) {
                try {
                    StackInfo pinnedStackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
                    if (pinnedStackInfo != null && pinnedStackInfo.taskIds != null &&
                            pinnedStackInfo.taskIds.length > 0) {
                        expandPipToFullscreen =
                                !pinnedStackInfo.topActivity.getPackageName().equals(
                ComponentName topActivity = PipUtils.getTopPinnedActivity(mActivityManager);
                expandPipToFullscreen = topActivity != null && topActivity.getPackageName().equals(
                        sourceComponent.getPackageName());
            }
                } catch (RemoteException e) {
                    Log.w(TAG, "Unable to get pinned stack.");
                }
            }
            if (expandPipToFullscreen) {
                mTouchHandler.expandPinnedStackToFullscreen();
            } else {
@@ -124,7 +118,7 @@ public class PipManager {
        @Override
        public void onActionsChanged(ParceledListSlice actions) {
            mHandler.post(() -> {
                mMenuController.setActions(actions);
                mMenuController.setAppActions(actions);
            });
        }

@@ -160,7 +154,9 @@ public class PipManager {
        }
        SystemServicesProxy.getInstance(mContext).registerTaskStackListener(mTaskStackListener);

        mMenuController = new PipMenuActivityController(context, mActivityManager, mWindowManager);
        mMediaController = new PipMediaController(context, mActivityManager);
        mMenuController = new PipMenuActivityController(context, mActivityManager, mWindowManager,
                mMediaController);
        mTouchHandler = new PipTouchHandler(context, mMenuController, mActivityManager,
                mWindowManager);
    }
+193 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.systemui.pip.phone;

import android.app.IActivityManager;
import android.app.RemoteAction;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.drawable.Icon;
import android.media.session.MediaController;
import android.media.session.MediaController.TransportControls;
import android.media.session.MediaSession;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;

import com.android.systemui.R;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Interfaces with the {@link MediaSessionManager} to compose the right set of actions to show (only
 * if there are no actions from the PiP activity itself). The active media controller is only set
 * when there is a media session from the top PiP activity.
 */
public class PipMediaController {

    /**
     * A listener interface to receive notification on changes to the media actions.
     */
    public interface ActionListener {
        /**
         * Called when the media actions changes.
         */
        void onMediaActionsChanged(List<RemoteAction> actions);
    }

    private final Context mContext;
    private final IActivityManager mActivityManager;

    private final MediaSessionManager mMediaSessionManager;
    private MediaController mMediaController;

    private RemoteAction mPauseAction;
    private RemoteAction mPlayAction;

    private MediaController.Callback mPlaybackChangedListener = new MediaController.Callback() {
        @Override
        public void onPlaybackStateChanged(PlaybackState state) {
            if (!mListeners.isEmpty()) {
                notifyActionsChanged(getMediaActions());
            }
        }
    };

    private ArrayList<ActionListener> mListeners = new ArrayList<>();

    public PipMediaController(Context context, IActivityManager activityManager) {
        mContext = context;
        mActivityManager = activityManager;

        createMediaActions();
        mMediaSessionManager =
                (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
        mMediaSessionManager.addOnActiveSessionsChangedListener(controllers -> {
            resolveActiveMediaController(controllers);
        }, null);
    }

    /**
     * Handles when an activity is pinned.
     */
    public void onActivityPinned() {
        // Once we enter PiP, try to find the active media controller for the top most activity
        resolveActiveMediaController(mMediaSessionManager.getActiveSessions(null));
    }

    /**
     * Adds a new media action listener.
     */
    public void addListener(ActionListener listener) {
        if (!mListeners.contains(listener)) {
            mListeners.add(listener);
            listener.onMediaActionsChanged(getMediaActions());
        }
    }

    /**
     * Removes a media action listener.
     */
    public void removeListener(ActionListener listener) {
        listener.onMediaActionsChanged(Collections.EMPTY_LIST);
        mListeners.remove(listener);
    }

    /**
     * Gets the set of media actions currently available.
     */
    private List<RemoteAction> getMediaActions() {
        if (mMediaController == null || mMediaController.getPlaybackState() == null) {
            return Collections.EMPTY_LIST;
        }

        ArrayList<RemoteAction> mediaActions = new ArrayList<>();
        int state = mMediaController.getPlaybackState().getState();
        boolean isPlaying = MediaSession.isActiveState(state);
        long actions = mMediaController.getPlaybackState().getActions();
        if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) {
            mediaActions.add(mPauseAction);
        } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) {
            mediaActions.add(mPlayAction);
        }
        return mediaActions;
    }

    /**
     * Creates the standard media buttons that we may show.
     */
    private void createMediaActions() {
        String pauseDescription = mContext.getString(R.string.pip_pause);
        mPauseAction = new RemoteAction(Icon.createWithResource(mContext,
                R.drawable.ic_pause_white_24dp), pauseDescription, pauseDescription,
                action -> mMediaController.getTransportControls().pause());

        String playDescription = mContext.getString(R.string.pip_play);
        mPlayAction = new RemoteAction(Icon.createWithResource(mContext,
                R.drawable.ic_play_arrow_white_24dp), playDescription, playDescription,
                action -> mMediaController.getTransportControls().play());
    }

    /**
     * Tries to find and set the active media controller for the top PiP activity.
     */
    private void resolveActiveMediaController(List<MediaController> controllers) {
        if (controllers != null) {
            final ComponentName topActivity = PipUtils.getTopPinnedActivity(mActivityManager);
            if (topActivity != null) {
                for (int i = 0; i < controllers.size(); i++) {
                    final MediaController controller = controllers.get(i);
                    if (controller.getPackageName().equals(topActivity.getPackageName())) {
                        setActiveMediaController(controller);
                        return;
                    }
                }
            }
        }
        setActiveMediaController(null);
    }

    /**
     * Sets the active media controller for the top PiP activity.
     */
    private void setActiveMediaController(MediaController controller) {
        if (controller != mMediaController) {
            if (mMediaController != null) {
                mMediaController.unregisterCallback(mPlaybackChangedListener);
            }
            mMediaController = controller;
            if (controller != null) {
                controller.registerCallback(mPlaybackChangedListener);
            }
            if (!mListeners.isEmpty()) {
                notifyActionsChanged(getMediaActions());
            }

            // TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV)
        }
    }

    /**
     * Notifies all listeners that the actions have changed.
     */
    private void notifyActionsChanged(List<RemoteAction> actions) {
        if (!mListeners.isEmpty()) {
            mListeners.forEach(l -> l.onMediaActionsChanged(actions));
        }
    }
}
+92 −12
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.systemui.pip.phone;

import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
@@ -5,6 +21,7 @@ import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
import android.app.ActivityManager.StackInfo;
import android.app.ActivityOptions;
import android.app.IActivityManager;
import android.app.RemoteAction;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ParceledListSlice;
@@ -16,11 +33,20 @@ import android.os.UserHandle;
import android.util.Log;
import android.view.IWindowManager;

import com.android.systemui.pip.phone.PipMediaController.ActionListener;

import java.util.ArrayList;
import java.util.List;

/**
 * Manages the PiP menu activity.
 *
 * The current media session provides actions whenever there are no valid actions provided by the
 * current PiP activity. Otherwise, those actions always take precedence.
 */
public class PipMenuActivityController {

    private static final String TAG = "PipMenuActivityController";
    private static final String TAG = "PipMenuActController";

    public static final String EXTRA_CONTROLLER_MESSENGER = "messenger";
    public static final String EXTRA_ACTIONS = "actions";
@@ -59,9 +85,12 @@ public class PipMenuActivityController {
    private Context mContext;
    private IActivityManager mActivityManager;
    private IWindowManager mWindowManager;
    private PipMediaController mMediaController;

    private ArrayList<Listener> mListeners = new ArrayList<>();
    private ParceledListSlice mActions;
    private ParceledListSlice mAppActions;
    private ParceledListSlice mMediaActions;
    private boolean mVisible;

    private Messenger mToActivityMessenger;
    private Messenger mMessenger = new Messenger(new Handler() {
@@ -70,13 +99,13 @@ public class PipMenuActivityController {
            switch (msg.what) {
                case MESSAGE_MENU_VISIBILITY_CHANGED: {
                    boolean visible = msg.arg1 > 0;
                    mListeners.forEach(l -> l.onPipMenuVisibilityChanged(visible));
                    onMenuVisibilityChanged(visible);
                    break;
                }
                case MESSAGE_EXPAND_PIP: {
                    mListeners.forEach(l -> l.onPipExpand());
                    // Preemptively mark the menu as invisible once we expand the PiP
                    mListeners.forEach(l -> l.onPipMenuVisibilityChanged(false));
                    onMenuVisibilityChanged(false);
                    break;
                }
                case MESSAGE_MINIMIZE_PIP: {
@@ -86,14 +115,14 @@ public class PipMenuActivityController {
                case MESSAGE_DISMISS_PIP: {
                    mListeners.forEach(l -> l.onPipDismiss());
                    // Preemptively mark the menu as invisible once we dismiss the PiP
                    mListeners.forEach(l -> l.onPipMenuVisibilityChanged(false));
                    onMenuVisibilityChanged(false);
                    break;
                }
                case MESSAGE_UPDATE_ACTIVITY_CALLBACK: {
                    mToActivityMessenger = msg.replyTo;
                    // Mark the menu as invisible once the activity finishes as well
                    if (mToActivityMessenger == null) {
                        mListeners.forEach(l -> l.onPipMenuVisibilityChanged(false));
                        onMenuVisibilityChanged(false);
                    }
                    break;
                }
@@ -101,11 +130,20 @@ public class PipMenuActivityController {
        }
    });

    private ActionListener mMediaActionListener = new ActionListener() {
        @Override
        public void onMediaActionsChanged(List<RemoteAction> mediaActions) {
            mMediaActions = new ParceledListSlice<>(mediaActions);
            updateMenuActions();
        }
    };

    public PipMenuActivityController(Context context, IActivityManager activityManager,
            IWindowManager windowManager) {
            IWindowManager windowManager, PipMediaController mediaController) {
        mContext = context;
        mActivityManager = activityManager;
        mWindowManager = windowManager;
        mMediaController = mediaController;
    }

    /**
@@ -137,7 +175,7 @@ public class PipMenuActivityController {
                        pinnedStackInfo.taskIds.length > 0) {
                    Intent intent = new Intent(mContext, PipMenuActivity.class);
                    intent.putExtra(EXTRA_CONTROLLER_MESSENGER, mMessenger);
                    intent.putExtra(EXTRA_ACTIONS, mActions);
                    intent.putExtra(EXTRA_ACTIONS, resolveMenuActions());
                    ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
                    options.setLaunchTaskId(
                            pinnedStackInfo.taskIds[pinnedStackInfo.taskIds.length - 1]);
@@ -168,15 +206,31 @@ public class PipMenuActivityController {
    }

    /**
     * Sets the {@param actions} associated with the PiP.
     * Sets the menu actions to the actions provided by the current PiP activity.
     */
    public void setActions(ParceledListSlice actions) {
        mActions = actions;
    public void setAppActions(ParceledListSlice appActions) {
        mAppActions = appActions;
        updateMenuActions();
    }

    /**
     * @return the best set of actions to show in the PiP menu.
     */
    private ParceledListSlice resolveMenuActions() {
        if (isValidActions(mAppActions)) {
            return mAppActions;
        }
        return mMediaActions;
    }

    /**
     * Updates the PiP menu activity with the best set of actions provided.
     */
    private void updateMenuActions() {
        if (mToActivityMessenger != null) {
            Message m = Message.obtain();
            m.what = PipMenuActivity.MESSAGE_UPDATE_ACTIONS;
            m.obj = actions;
            m.obj = resolveMenuActions();
            try {
                mToActivityMessenger.send(m);
            } catch (RemoteException e) {
@@ -184,4 +238,30 @@ public class PipMenuActivityController {
            }
        }
    }

    /**
     * Returns whether the set of actions are valid.
     */
    private boolean isValidActions(ParceledListSlice actions) {
        return actions != null && actions.getList().size() > 0;
    }

    /**
     * Handles changes in menu visibility.
     */
    private void onMenuVisibilityChanged(boolean visible) {
        mListeners.forEach(l -> l.onPipMenuVisibilityChanged(visible));
        if (visible != mVisible) {
            if (visible) {
                // Once visible, start listening for media action changes. This call will trigger
                // the menu actions to be updated again.
                mMediaController.addListener(mMediaActionListener);
            } else {
                // Once hidden, stop listening for media action changes. This call will trigger
                // the menu actions to be updated again.
                mMediaController.removeListener(mMediaActionListener);
            }
        }
        mVisible = visible;
    }
}
+46 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.systemui.pip.phone;

import static android.app.ActivityManager.StackId.PINNED_STACK_ID;

import android.app.ActivityManager.StackInfo;
import android.app.IActivityManager;
import android.content.ComponentName;
import android.os.RemoteException;
import android.util.Log;

public class PipUtils {

    private static final String TAG = "PipUtils";

    /**
     * @return the ComponentName of the top activity in the pinned stack, or null if none exists.
     */
    public static ComponentName getTopPinnedActivity(IActivityManager activityManager) {
        try {
            StackInfo pinnedStackInfo = activityManager.getStackInfo(PINNED_STACK_ID);
            if (pinnedStackInfo != null && pinnedStackInfo.taskIds != null &&
                    pinnedStackInfo.taskIds.length > 0) {
                return pinnedStackInfo.topActivity;
            }
        } catch (RemoteException e) {
            Log.w(TAG, "Unable to get pinned stack.");
        }
        return null;
    }
}