All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.wm.shell.pip.PipMediaController Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * Copyright (C) 2020 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;

import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;

import android.annotation.DrawableRes;
import android.annotation.StringRes;
import android.annotation.SuppressLint;
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.graphics.drawable.Icon;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.UserHandle;

import androidx.annotation.Nullable;

import com.android.wm.shell.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 {
    private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";

    private static final String ACTION_PLAY = "com.android.wm.shell.pip.PLAY";
    private static final String ACTION_PAUSE = "com.android.wm.shell.pip.PAUSE";
    private static final String ACTION_NEXT = "com.android.wm.shell.pip.NEXT";
    private static final String ACTION_PREV = "com.android.wm.shell.pip.PREV";

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

    /**
     * A listener interface to receive notification on changes to the media metadata.
     */
    public interface MetadataListener {
        /**
         * Called when the media metadata changes.
         */
        void onMediaMetadataChanged(MediaMetadata metadata);
    }

    private final Context mContext;
    private final Handler mMainHandler;
    private final HandlerExecutor mHandlerExecutor;

    private final MediaSessionManager mMediaSessionManager;
    private MediaController mMediaController;

    private RemoteAction mPauseAction;
    private RemoteAction mPlayAction;
    private RemoteAction mNextAction;
    private RemoteAction mPrevAction;

    private final BroadcastReceiver mMediaActionReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (mMediaController == null || mMediaController.getTransportControls() == null) {
                // no active media session, bail early.
                return;
            }
            switch (intent.getAction()) {
                case ACTION_PLAY:
                    mMediaController.getTransportControls().play();
                    break;
                case ACTION_PAUSE:
                    mMediaController.getTransportControls().pause();
                    break;
                case ACTION_NEXT:
                    mMediaController.getTransportControls().skipToNext();
                    break;
                case ACTION_PREV:
                    mMediaController.getTransportControls().skipToPrevious();
                    break;
            }
        }
    };

    private final MediaController.Callback mPlaybackChangedListener =
            new MediaController.Callback() {
                @Override
                public void onPlaybackStateChanged(PlaybackState state) {
                    notifyActionsChanged();
                }

                @Override
                public void onMetadataChanged(@Nullable MediaMetadata metadata) {
                    notifyMetadataChanged(metadata);
                }
            };

    private final MediaSessionManager.OnActiveSessionsChangedListener mSessionsChangedListener =
            this::resolveActiveMediaController;

    private final ArrayList mActionListeners = new ArrayList<>();
    private final ArrayList mMetadataListeners = new ArrayList<>();

    public PipMediaController(Context context, Handler mainHandler) {
        mContext = context;
        mMainHandler = mainHandler;
        mHandlerExecutor = new HandlerExecutor(mMainHandler);
        IntentFilter mediaControlFilter = new IntentFilter();
        mediaControlFilter.addAction(ACTION_PLAY);
        mediaControlFilter.addAction(ACTION_PAUSE);
        mediaControlFilter.addAction(ACTION_NEXT);
        mediaControlFilter.addAction(ACTION_PREV);
        mContext.registerReceiverForAllUsers(mMediaActionReceiver, mediaControlFilter,
                SYSTEMUI_PERMISSION, mainHandler);

        // Creates the standard media buttons that we may show.
        mPauseAction = getDefaultRemoteAction(R.string.pip_pause,
                R.drawable.pip_ic_pause_white, ACTION_PAUSE);
        mPlayAction = getDefaultRemoteAction(R.string.pip_play,
                R.drawable.pip_ic_play_arrow_white, ACTION_PLAY);
        mNextAction = getDefaultRemoteAction(R.string.pip_skip_to_next,
                R.drawable.pip_ic_skip_next_white, ACTION_NEXT);
        mPrevAction = getDefaultRemoteAction(R.string.pip_skip_to_prev,
                R.drawable.pip_ic_skip_previous_white, ACTION_PREV);

        mMediaSessionManager = context.getSystemService(MediaSessionManager.class);
    }

    /**
     * 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.getActiveSessionsForUser(null,
                UserHandle.CURRENT));
    }

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

    /**
     * Removes a media action listener.
     */
    public void removeActionListener(ActionListener listener) {
        listener.onMediaActionsChanged(Collections.emptyList());
        mActionListeners.remove(listener);
    }

    /**
     * Adds a new media metadata listener.
     */
    public void addMetadataListener(MetadataListener listener) {
        if (!mMetadataListeners.contains(listener)) {
            mMetadataListeners.add(listener);
            listener.onMediaMetadataChanged(getMediaMetadata());
        }
    }

    /**
     * Removes a media metadata listener.
     */
    public void removeMetadataListener(MetadataListener listener) {
        listener.onMediaMetadataChanged(null);
        mMetadataListeners.remove(listener);
    }

    private MediaMetadata getMediaMetadata() {
        return mMediaController != null ? mMediaController.getMetadata() : null;
    }

    /**
     * Gets the set of media actions currently available.
     */
    // This is due to using PlaybackState#isActive, which is added in API 31.
    // It can be removed when min_sdk of the app is set to 31 or greater.
    @SuppressLint("NewApi")
    private List getMediaActions() {
        if (mMediaController == null || mMediaController.getPlaybackState() == null) {
            return Collections.emptyList();
        }

        ArrayList mediaActions = new ArrayList<>();
        boolean isPlaying = mMediaController.getPlaybackState().isActive();
        long actions = mMediaController.getPlaybackState().getActions();

        // Prev action
        mPrevAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0);
        mediaActions.add(mPrevAction);

        // Play/pause action
        if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) {
            mediaActions.add(mPlayAction);
        } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) {
            mediaActions.add(mPauseAction);
        }

        // Next action
        mNextAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0);
        mediaActions.add(mNextAction);
        return mediaActions;
    }

    /** @return Default {@link RemoteAction} sends broadcast back to SysUI. */
    private RemoteAction getDefaultRemoteAction(@StringRes int titleAndDescription,
            @DrawableRes int icon, String action) {
        final String titleAndDescriptionStr = mContext.getString(titleAndDescription);
        final Intent intent = new Intent(action);
        intent.setPackage(mContext.getPackageName());
        return new RemoteAction(Icon.createWithResource(mContext, icon),
                titleAndDescriptionStr, titleAndDescriptionStr,
                PendingIntent.getBroadcast(mContext, 0 /* requestCode */, intent,
                        FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE));
    }

    /**
     * Re-registers the session listener for the current user.
     */
    public void registerSessionListenerForCurrentUser() {
        mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsChangedListener);
        mMediaSessionManager.addOnActiveSessionsChangedListener(null, UserHandle.CURRENT,
                mHandlerExecutor, mSessionsChangedListener);
    }

    /**
     * Tries to find and set the active media controller for the top PiP activity.
     */
    private void resolveActiveMediaController(List controllers) {
        if (controllers != null) {
            final ComponentName topActivity = PipUtils.getTopPipActivity(mContext).first;
            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, mMainHandler);
            }
            notifyActionsChanged();
            notifyMetadataChanged(getMediaMetadata());

            // 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() {
        if (!mActionListeners.isEmpty()) {
            List actions = getMediaActions();
            mActionListeners.forEach(l -> l.onMediaActionsChanged(actions));
        }
    }

    /**
     * Notifies all listeners that the metadata have changed.
     */
    private void notifyMetadataChanged(MediaMetadata metadata) {
        if (!mMetadataListeners.isEmpty()) {
            mMetadataListeners.forEach(l -> l.onMediaMetadataChanged(metadata));
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy