public class

RemotePlaybackClient

extends java.lang.Object

 java.lang.Object

↳androidx.mediarouter.media.RemotePlaybackClient

Gradle dependencies

compile group: 'androidx.mediarouter', name: 'mediarouter', version: '1.3.0'

  • groupId: androidx.mediarouter
  • artifactId: mediarouter
  • version: 1.3.0

Artifact androidx.mediarouter:mediarouter:1.3.0 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.mediarouter:mediarouter com.android.support:mediarouter-v7

Androidx class mapping:

androidx.mediarouter.media.RemotePlaybackClient android.support.v7.media.RemotePlaybackClient

Overview

A helper class for playing media on remote routes using the remote playback protocol defined by MediaControlIntent.

The client maintains session state and offers a simplified interface for issuing remote playback media control intents to a single route.

Summary

Constructors
publicRemotePlaybackClient(Context context, MediaRouter.RouteInfo route)

Creates a remote playback client for a route.

Methods
public voidendSession(Bundle extras, RemotePlaybackClient.SessionActionCallback callback)

Sends a request to end the media playback session.

public voidenqueue(Uri contentUri, java.lang.String mimeType, Bundle metadata, long positionMillis, Bundle extras, RemotePlaybackClient.ItemActionCallback callback)

Sends a request to enqueue a media item.

public java.lang.StringgetSessionId()

Gets the current session id if there is one.

public voidgetSessionStatus(Bundle extras, RemotePlaybackClient.SessionActionCallback callback)

Sends a request to get the status of the media playback session.

public voidgetStatus(java.lang.String itemId, Bundle extras, RemotePlaybackClient.ItemActionCallback callback)

Sends a request to get the status of a media item.

public booleanhasSession()

Returns true if the client currently has a session.

public booleanisMessagingSupported()

Returns true if the route supports messages.

public booleanisQueuingSupported()

Returns true if the route supports queuing features.

public booleanisRemotePlaybackSupported()

Returns true if the route supports remote playback.

public booleanisSessionManagementSupported()

Returns true if the route supports session management features.

public voidpause(Bundle extras, RemotePlaybackClient.SessionActionCallback callback)

Sends a request to pause media playback.

public voidplay(Uri contentUri, java.lang.String mimeType, Bundle metadata, long positionMillis, Bundle extras, RemotePlaybackClient.ItemActionCallback callback)

Sends a request to play a media item.

public voidrelease()

Releases resources owned by the client.

public voidremove(java.lang.String itemId, Bundle extras, RemotePlaybackClient.ItemActionCallback callback)

Sends a request to remove a media item from the queue.

public voidresume(Bundle extras, RemotePlaybackClient.SessionActionCallback callback)

Sends a request to resume (unpause) media playback.

public voidseek(java.lang.String itemId, long positionMillis, Bundle extras, RemotePlaybackClient.ItemActionCallback callback)

Sends a request to seek to a new position in a media item.

public voidsendMessage(Bundle message, RemotePlaybackClient.SessionActionCallback callback)

Sends a message.

public voidsetOnMessageReceivedListener(RemotePlaybackClient.OnMessageReceivedListener listener)

Sets a callback that should receive messages when a message is sent from media sessions created by this instance of the remote playback client changes.

public voidsetSessionId(java.lang.String sessionId)

Sets the current session id.

public voidsetStatusCallback(RemotePlaybackClient.StatusCallback callback)

Sets a callback that should receive status updates when the state of media sessions or media items created by this instance of the remote playback client changes.

public voidstartSession(Bundle extras, RemotePlaybackClient.SessionActionCallback callback)

Sends a request to start a new media playback session.

public voidstop(Bundle extras, RemotePlaybackClient.SessionActionCallback callback)

Sends a request to stop media playback and clear the media playback queue.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public RemotePlaybackClient(Context context, MediaRouter.RouteInfo route)

Creates a remote playback client for a route.

Parameters:

route: The media route.

Methods

public void release()

Releases resources owned by the client.

public boolean isRemotePlaybackSupported()

Returns true if the route supports remote playback.

If the route does not support remote playback, then none of the functionality offered by the client will be available.

This method returns true if the route supports all of the following actions: play, seek, get status, pause, resume, stop.

Returns:

True if remote playback is supported.

public boolean isQueuingSupported()

Returns true if the route supports queuing features.

If the route does not support queuing, then at most one media item can be played at a time and the RemotePlaybackClient.enqueue(Uri, String, Bundle, long, Bundle, RemotePlaybackClient.ItemActionCallback) method will not be available.

This method returns true if the route supports all of the basic remote playback actions and all of the following actions: enqueue, remove.

Returns:

True if queuing is supported. Implies RemotePlaybackClient.isRemotePlaybackSupported() is also true.

See also: RemotePlaybackClient.isRemotePlaybackSupported()

public boolean isSessionManagementSupported()

Returns true if the route supports session management features.

If the route does not support session management, then the session will not be created until the first media item is played.

This method returns true if the route supports all of the basic remote playback actions and all of the following actions: start session, get session status, end session.

Returns:

True if session management is supported. Implies RemotePlaybackClient.isRemotePlaybackSupported() is also true.

See also: RemotePlaybackClient.isRemotePlaybackSupported()

public boolean isMessagingSupported()

Returns true if the route supports messages.

This method returns true if the route supports all of the basic remote playback actions and all of the following actions: start session, send message, end session.

Returns:

True if session management is supported. Implies RemotePlaybackClient.isRemotePlaybackSupported() is also true.

See also: RemotePlaybackClient.isRemotePlaybackSupported()

public java.lang.String getSessionId()

Gets the current session id if there is one.

Returns:

The current session id, or null if none.

public void setSessionId(java.lang.String sessionId)

Sets the current session id.

It is usually not necessary to set the session id explicitly since it is created as a side-effect of other requests such as RemotePlaybackClient.play(Uri, String, Bundle, long, Bundle, RemotePlaybackClient.ItemActionCallback), RemotePlaybackClient.enqueue(Uri, String, Bundle, long, Bundle, RemotePlaybackClient.ItemActionCallback), and RemotePlaybackClient.startSession(Bundle, RemotePlaybackClient.SessionActionCallback).

Parameters:

sessionId: The new session id, or null if none.

public boolean hasSession()

Returns true if the client currently has a session.

Equivalent to checking whether RemotePlaybackClient.getSessionId() returns a non-null result.

Returns:

True if there is a current session.

public void setStatusCallback(RemotePlaybackClient.StatusCallback callback)

Sets a callback that should receive status updates when the state of media sessions or media items created by this instance of the remote playback client changes.

The callback should be set before the session is created or any play commands are issued.

Parameters:

callback: The callback to set. May be null to remove the previous callback.

public void setOnMessageReceivedListener(RemotePlaybackClient.OnMessageReceivedListener listener)

Sets a callback that should receive messages when a message is sent from media sessions created by this instance of the remote playback client changes.

The callback should be set before the session is created.

Parameters:

listener: The callback to set. May be null to remove the previous callback.

public void play(Uri contentUri, java.lang.String mimeType, Bundle metadata, long positionMillis, Bundle extras, RemotePlaybackClient.ItemActionCallback callback)

Sends a request to play a media item.

Clears the queue and starts playing the new item immediately. If the queue was previously paused, then it is resumed as a side-effect of this request.

The request is issued in the current session. If no session is available, then one is created implicitly.

Please refer to ACTION_PLAY for more information about the semantics of this request.

Parameters:

contentUri: The content Uri to play.
mimeType: The mime type of the content, or null if unknown.
positionMillis: The initial content position for the item in milliseconds, or 0 to start at the beginning.
metadata: The media item metadata bundle, or null if none.
extras: A bundle of extra arguments to be added to the MediaControlIntent.ACTION_PLAY intent, or null if none.
callback: A callback to invoke when the request has been processed, or null if none.

See also: MediaControlIntent.ACTION_PLAY, RemotePlaybackClient.isRemotePlaybackSupported()

public void enqueue(Uri contentUri, java.lang.String mimeType, Bundle metadata, long positionMillis, Bundle extras, RemotePlaybackClient.ItemActionCallback callback)

Sends a request to enqueue a media item.

Enqueues a new item to play. If the queue was previously paused, then will remain paused.

The request is issued in the current session. If no session is available, then one is created implicitly.

Please refer to ACTION_ENQUEUE for more information about the semantics of this request.

Parameters:

contentUri: The content Uri to enqueue.
mimeType: The mime type of the content, or null if unknown.
positionMillis: The initial content position for the item in milliseconds, or 0 to start at the beginning.
metadata: The media item metadata bundle, or null if none.
extras: A bundle of extra arguments to be added to the MediaControlIntent.ACTION_ENQUEUE intent, or null if none.
callback: A callback to invoke when the request has been processed, or null if none.

See also: MediaControlIntent.ACTION_ENQUEUE, RemotePlaybackClient.isRemotePlaybackSupported(), RemotePlaybackClient.isQueuingSupported()

public void seek(java.lang.String itemId, long positionMillis, Bundle extras, RemotePlaybackClient.ItemActionCallback callback)

Sends a request to seek to a new position in a media item.

Seeks to a new position. If the queue was previously paused then it remains paused but the item's new position is still remembered.

The request is issued in the current session.

Please refer to ACTION_SEEK for more information about the semantics of this request.

Parameters:

itemId: The item id.
positionMillis: The new content position for the item in milliseconds, or 0 to start at the beginning.
extras: A bundle of extra arguments to be added to the MediaControlIntent.ACTION_SEEK intent, or null if none.
callback: A callback to invoke when the request has been processed, or null if none.

See also: MediaControlIntent.ACTION_SEEK, RemotePlaybackClient.isRemotePlaybackSupported()

public void getStatus(java.lang.String itemId, Bundle extras, RemotePlaybackClient.ItemActionCallback callback)

Sends a request to get the status of a media item.

The request is issued in the current session.

Please refer to ACTION_GET_STATUS for more information about the semantics of this request.

Parameters:

itemId: The item id.
extras: A bundle of extra arguments to be added to the MediaControlIntent.ACTION_GET_STATUS intent, or null if none.
callback: A callback to invoke when the request has been processed, or null if none.

See also: MediaControlIntent.ACTION_GET_STATUS, RemotePlaybackClient.isRemotePlaybackSupported()

public void remove(java.lang.String itemId, Bundle extras, RemotePlaybackClient.ItemActionCallback callback)

Sends a request to remove a media item from the queue.

The request is issued in the current session.

Please refer to ACTION_REMOVE for more information about the semantics of this request.

Parameters:

itemId: The item id.
extras: A bundle of extra arguments to be added to the MediaControlIntent.ACTION_REMOVE intent, or null if none.
callback: A callback to invoke when the request has been processed, or null if none.

See also: MediaControlIntent.ACTION_REMOVE, RemotePlaybackClient.isRemotePlaybackSupported(), RemotePlaybackClient.isQueuingSupported()

public void pause(Bundle extras, RemotePlaybackClient.SessionActionCallback callback)

Sends a request to pause media playback.

The request is issued in the current session. If playback is already paused then the request has no effect.

Please refer to ACTION_PAUSE for more information about the semantics of this request.

Parameters:

extras: A bundle of extra arguments to be added to the MediaControlIntent.ACTION_PAUSE intent, or null if none.
callback: A callback to invoke when the request has been processed, or null if none.

See also: MediaControlIntent.ACTION_PAUSE, RemotePlaybackClient.isRemotePlaybackSupported()

public void resume(Bundle extras, RemotePlaybackClient.SessionActionCallback callback)

Sends a request to resume (unpause) media playback.

The request is issued in the current session. If playback is not paused then the request has no effect.

Please refer to ACTION_RESUME for more information about the semantics of this request.

Parameters:

extras: A bundle of extra arguments to be added to the MediaControlIntent.ACTION_RESUME intent, or null if none.
callback: A callback to invoke when the request has been processed, or null if none.

See also: MediaControlIntent.ACTION_RESUME, RemotePlaybackClient.isRemotePlaybackSupported()

public void stop(Bundle extras, RemotePlaybackClient.SessionActionCallback callback)

Sends a request to stop media playback and clear the media playback queue.

The request is issued in the current session. If the queue is already empty then the request has no effect.

Please refer to ACTION_STOP for more information about the semantics of this request.

Parameters:

extras: A bundle of extra arguments to be added to the MediaControlIntent.ACTION_STOP intent, or null if none.
callback: A callback to invoke when the request has been processed, or null if none.

See also: MediaControlIntent.ACTION_STOP, RemotePlaybackClient.isRemotePlaybackSupported()

public void startSession(Bundle extras, RemotePlaybackClient.SessionActionCallback callback)

Sends a request to start a new media playback session.

The application must wait for the callback to indicate that this request is complete before issuing other requests that affect the session. If this request is successful then the previous session will be invalidated.

Please refer to ACTION_START_SESSION for more information about the semantics of this request.

Parameters:

extras: A bundle of extra arguments to be added to the MediaControlIntent.ACTION_START_SESSION intent, or null if none.
callback: A callback to invoke when the request has been processed, or null if none.

See also: MediaControlIntent.ACTION_START_SESSION, RemotePlaybackClient.isRemotePlaybackSupported(), RemotePlaybackClient.isSessionManagementSupported()

public void sendMessage(Bundle message, RemotePlaybackClient.SessionActionCallback callback)

Sends a message.

The request is issued in the current session.

Please refer to MediaControlIntent.ACTION_SEND_MESSAGE for more information about the semantics of this request.

Parameters:

message: A bundle message denoting MediaControlIntent.EXTRA_MESSAGE.
callback: A callback to invoke when the request has been processed, or null if none.

See also: MediaControlIntent.ACTION_SEND_MESSAGE, RemotePlaybackClient.isMessagingSupported()

public void getSessionStatus(Bundle extras, RemotePlaybackClient.SessionActionCallback callback)

Sends a request to get the status of the media playback session.

The request is issued in the current session.

Please refer to ACTION_GET_SESSION_STATUS for more information about the semantics of this request.

Parameters:

extras: A bundle of extra arguments to be added to the MediaControlIntent.ACTION_GET_SESSION_STATUS intent, or null if none.
callback: A callback to invoke when the request has been processed, or null if none.

See also: MediaControlIntent.ACTION_GET_SESSION_STATUS, RemotePlaybackClient.isRemotePlaybackSupported(), RemotePlaybackClient.isSessionManagementSupported()

public void endSession(Bundle extras, RemotePlaybackClient.SessionActionCallback callback)

Sends a request to end the media playback session.

The request is issued in the current session. If this request is successful, the session id property will be set to null after the callback is invoked.

Please refer to ACTION_END_SESSION for more information about the semantics of this request.

Parameters:

extras: A bundle of extra arguments to be added to the MediaControlIntent.ACTION_END_SESSION intent, or null if none.
callback: A callback to invoke when the request has been processed, or null if none.

See also: MediaControlIntent.ACTION_END_SESSION, RemotePlaybackClient.isRemotePlaybackSupported(), RemotePlaybackClient.isSessionManagementSupported()

Source

/*
 * Copyright (C) 2013 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 androidx.mediarouter.media;

import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.ObjectsCompat;

/**
 * A helper class for playing media on remote routes using the remote playback protocol
 * defined by {@link MediaControlIntent}.
 * <p>
 * The client maintains session state and offers a simplified interface for issuing
 * remote playback media control intents to a single route.
 * </p>
 */
public class RemotePlaybackClient {
    static final String TAG = "RemotePlaybackClient";
    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private final Context mContext;
    private final MediaRouter.RouteInfo mRoute;
    private final ActionReceiver mActionReceiver;
    private final PendingIntent mItemStatusPendingIntent;
    private final PendingIntent mSessionStatusPendingIntent;
    private final PendingIntent mMessagePendingIntent;

    private boolean mRouteSupportsRemotePlayback;
    private boolean mRouteSupportsQueuing;
    private boolean mRouteSupportsSessionManagement;
    private boolean mRouteSupportsMessaging;

    String mSessionId;
    StatusCallback mStatusCallback;
    OnMessageReceivedListener mOnMessageReceivedListener;

    /**
     * Creates a remote playback client for a route.
     *
     * @param route The media route.
     */
    public RemotePlaybackClient(@NonNull Context context, @NonNull MediaRouter.RouteInfo route) {
        if (context == null) {
            throw new IllegalArgumentException("context must not be null");
        }
        if (route == null) {
            throw new IllegalArgumentException("route must not be null");
        }

        mContext = context;
        mRoute = route;

        IntentFilter actionFilter = new IntentFilter();
        actionFilter.addAction(ActionReceiver.ACTION_ITEM_STATUS_CHANGED);
        actionFilter.addAction(ActionReceiver.ACTION_SESSION_STATUS_CHANGED);
        actionFilter.addAction(ActionReceiver.ACTION_MESSAGE_RECEIVED);
        mActionReceiver = new ActionReceiver();
        context.registerReceiver(mActionReceiver, actionFilter);

        Intent itemStatusIntent = new Intent(ActionReceiver.ACTION_ITEM_STATUS_CHANGED);
        itemStatusIntent.setPackage(context.getPackageName());
        mItemStatusPendingIntent = PendingIntent.getBroadcast(
                context, 0, itemStatusIntent, PendingIntent.FLAG_IMMUTABLE);

        Intent sessionStatusIntent = new Intent(ActionReceiver.ACTION_SESSION_STATUS_CHANGED);
        sessionStatusIntent.setPackage(context.getPackageName());
        mSessionStatusPendingIntent = PendingIntent.getBroadcast(
                context, 0, sessionStatusIntent, PendingIntent.FLAG_IMMUTABLE);

        Intent messageIntent = new Intent(ActionReceiver.ACTION_MESSAGE_RECEIVED);
        messageIntent.setPackage(context.getPackageName());
        mMessagePendingIntent = PendingIntent.getBroadcast(
                context, 0, messageIntent, PendingIntent.FLAG_IMMUTABLE);
        detectFeatures();
    }

    /**
     * Releases resources owned by the client.
     */
    public void release() {
        mContext.unregisterReceiver(mActionReceiver);
    }

    /**
     * Returns true if the route supports remote playback.
     * <p>
     * If the route does not support remote playback, then none of the functionality
     * offered by the client will be available.
     * </p><p>
     * This method returns true if the route supports all of the following
     * actions: {@link MediaControlIntent#ACTION_PLAY play},
     * {@link MediaControlIntent#ACTION_SEEK seek},
     * {@link MediaControlIntent#ACTION_GET_STATUS get status},
     * {@link MediaControlIntent#ACTION_PAUSE pause},
     * {@link MediaControlIntent#ACTION_RESUME resume},
     * {@link MediaControlIntent#ACTION_STOP stop}.
     * </p>
     *
     * @return True if remote playback is supported.
     */
    public boolean isRemotePlaybackSupported() {
        return mRouteSupportsRemotePlayback;
    }

    /**
     * Returns true if the route supports queuing features.
     * <p>
     * If the route does not support queuing, then at most one media item can be played
     * at a time and the {@link #enqueue} method will not be available.
     * </p><p>
     * This method returns true if the route supports all of the basic remote playback
     * actions and all of the following actions:
     * {@link MediaControlIntent#ACTION_ENQUEUE enqueue},
     * {@link MediaControlIntent#ACTION_REMOVE remove}.
     * </p>
     *
     * @return True if queuing is supported.  Implies {@link #isRemotePlaybackSupported}
     * is also true.
     *
     * @see #isRemotePlaybackSupported
     */
    public boolean isQueuingSupported() {
        return mRouteSupportsQueuing;
    }

    /**
     * Returns true if the route supports session management features.
     * <p>
     * If the route does not support session management, then the session will
     * not be created until the first media item is played.
     * </p><p>
     * This method returns true if the route supports all of the basic remote playback
     * actions and all of the following actions:
     * {@link MediaControlIntent#ACTION_START_SESSION start session},
     * {@link MediaControlIntent#ACTION_GET_SESSION_STATUS get session status},
     * {@link MediaControlIntent#ACTION_END_SESSION end session}.
     * </p>
     *
     * @return True if session management is supported.
     * Implies {@link #isRemotePlaybackSupported} is also true.
     *
     * @see #isRemotePlaybackSupported
     */
    public boolean isSessionManagementSupported() {
        return mRouteSupportsSessionManagement;
    }

    /**
     * Returns true if the route supports messages.
     * <p>
     * This method returns true if the route supports all of the basic remote playback
     * actions and all of the following actions:
     * {@link MediaControlIntent#ACTION_START_SESSION start session},
     * {@link MediaControlIntent#ACTION_SEND_MESSAGE send message},
     * {@link MediaControlIntent#ACTION_END_SESSION end session}.
     * </p>
     *
     * @return True if session management is supported.
     * Implies {@link #isRemotePlaybackSupported} is also true.
     *
     * @see #isRemotePlaybackSupported
     */
    public boolean isMessagingSupported() {
        return mRouteSupportsMessaging;
    }

    /**
     * Gets the current session id if there is one.
     *
     * @return The current session id, or null if none.
     */
    @Nullable
    public String getSessionId() {
        return mSessionId;
    }

    /**
     * Sets the current session id.
     * <p>
     * It is usually not necessary to set the session id explicitly since
     * it is created as a side-effect of other requests such as
     * {@link #play}, {@link #enqueue}, and {@link #startSession}.
     * </p>
     *
     * @param sessionId The new session id, or null if none.
     */
    public void setSessionId(@Nullable String sessionId) {
        if (!ObjectsCompat.equals(mSessionId, sessionId)) {
            if (DEBUG) {
                Log.d(TAG, "Session id is now: " + sessionId);
            }
            mSessionId = sessionId;
            if (mStatusCallback != null) {
                mStatusCallback.onSessionChanged(sessionId);
            }
        }
    }

    /**
     * Returns true if the client currently has a session.
     * <p>
     * Equivalent to checking whether {@link #getSessionId} returns a non-null result.
     * </p>
     *
     * @return True if there is a current session.
     */
    public boolean hasSession() {
        return mSessionId != null;
    }

    /**
     * Sets a callback that should receive status updates when the state of
     * media sessions or media items created by this instance of the remote
     * playback client changes.
     * <p>
     * The callback should be set before the session is created or any play
     * commands are issued.
     * </p>
     *
     * @param callback The callback to set.  May be null to remove the previous callback.
     */
    public void setStatusCallback(@Nullable StatusCallback callback) {
        mStatusCallback = callback;
    }

    /**
     * Sets a callback that should receive messages when a message is sent from
     * media sessions created by this instance of the remote playback client changes.
     * <p>
     * The callback should be set before the session is created.
     * </p>
     *
     * @param listener The callback to set.  May be null to remove the previous callback.
     */
    public void setOnMessageReceivedListener(@Nullable OnMessageReceivedListener listener) {
        mOnMessageReceivedListener = listener;
    }

    /**
     * Sends a request to play a media item.
     * <p>
     * Clears the queue and starts playing the new item immediately.  If the queue
     * was previously paused, then it is resumed as a side-effect of this request.
     * </p><p>
     * The request is issued in the current session.  If no session is available, then
     * one is created implicitly.
     * </p><p>
     * Please refer to {@link MediaControlIntent#ACTION_PLAY ACTION_PLAY} for
     * more information about the semantics of this request.
     * </p>
     *
     * @param contentUri The content Uri to play.
     * @param mimeType The mime type of the content, or null if unknown.
     * @param positionMillis The initial content position for the item in milliseconds,
     * or <code>0</code> to start at the beginning.
     * @param metadata The media item metadata bundle, or null if none.
     * @param extras A bundle of extra arguments to be added to the
     * {@link MediaControlIntent#ACTION_PLAY} intent, or null if none.
     * @param callback A callback to invoke when the request has been
     * processed, or null if none.
     *
     * @throws UnsupportedOperationException if the route does not support remote playback.
     *
     * @see MediaControlIntent#ACTION_PLAY
     * @see #isRemotePlaybackSupported
     */
    public void play(@NonNull Uri contentUri, @Nullable String mimeType, @Nullable Bundle metadata,
            long positionMillis, @Nullable Bundle extras, @Nullable ItemActionCallback callback) {
        playOrEnqueue(contentUri, mimeType, metadata, positionMillis,
                extras, callback, MediaControlIntent.ACTION_PLAY);
    }

    /**
     * Sends a request to enqueue a media item.
     * <p>
     * Enqueues a new item to play.  If the queue was previously paused, then will
     * remain paused.
     * </p><p>
     * The request is issued in the current session.  If no session is available, then
     * one is created implicitly.
     * </p><p>
     * Please refer to {@link MediaControlIntent#ACTION_ENQUEUE ACTION_ENQUEUE} for
     * more information about the semantics of this request.
     * </p>
     *
     * @param contentUri The content Uri to enqueue.
     * @param mimeType The mime type of the content, or null if unknown.
     * @param positionMillis The initial content position for the item in milliseconds,
     * or <code>0</code> to start at the beginning.
     * @param metadata The media item metadata bundle, or null if none.
     * @param extras A bundle of extra arguments to be added to the
     * {@link MediaControlIntent#ACTION_ENQUEUE} intent, or null if none.
     * @param callback A callback to invoke when the request has been
     * processed, or null if none.
     *
     * @throws UnsupportedOperationException if the route does not support queuing.
     *
     * @see MediaControlIntent#ACTION_ENQUEUE
     * @see #isRemotePlaybackSupported
     * @see #isQueuingSupported
     */
    public void enqueue(@NonNull Uri contentUri, @Nullable String mimeType,
            @Nullable Bundle metadata, long positionMillis, @Nullable Bundle extras,
            @Nullable ItemActionCallback callback) {
        playOrEnqueue(contentUri, mimeType, metadata, positionMillis,
                extras, callback, MediaControlIntent.ACTION_ENQUEUE);
    }

    private void playOrEnqueue(Uri contentUri, String mimeType, Bundle metadata,
            long positionMillis, Bundle extras,
            final ItemActionCallback callback, String action) {
        if (contentUri == null) {
            throw new IllegalArgumentException("contentUri must not be null");
        }
        throwIfRemotePlaybackNotSupported();
        if (action.equals(MediaControlIntent.ACTION_ENQUEUE)) {
            throwIfQueuingNotSupported();
        }

        Intent intent = new Intent(action);
        intent.setDataAndType(contentUri, mimeType);
        intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER,
                mItemStatusPendingIntent);
        if (metadata != null) {
            intent.putExtra(MediaControlIntent.EXTRA_ITEM_METADATA, metadata);
        }
        if (positionMillis != 0) {
            intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, positionMillis);
        }
        performItemAction(intent, mSessionId, null, extras, callback);
    }

    /**
     * Sends a request to seek to a new position in a media item.
     * <p>
     * Seeks to a new position.  If the queue was previously paused then it
     * remains paused but the item's new position is still remembered.
     * </p><p>
     * The request is issued in the current session.
     * </p><p>
     * Please refer to {@link MediaControlIntent#ACTION_SEEK ACTION_SEEK} for
     * more information about the semantics of this request.
     * </p>
     *
     * @param itemId The item id.
     * @param positionMillis The new content position for the item in milliseconds,
     * or <code>0</code> to start at the beginning.
     * @param extras A bundle of extra arguments to be added to the
     * {@link MediaControlIntent#ACTION_SEEK} intent, or null if none.
     * @param callback A callback to invoke when the request has been
     * processed, or null if none.
     *
     * @throws IllegalStateException if there is no current session.
     *
     * @see MediaControlIntent#ACTION_SEEK
     * @see #isRemotePlaybackSupported
     */
    public void seek(@NonNull String itemId, long positionMillis, @Nullable Bundle extras,
            @Nullable ItemActionCallback callback) {
        if (itemId == null) {
            throw new IllegalArgumentException("itemId must not be null");
        }
        throwIfNoCurrentSession();

        Intent intent = new Intent(MediaControlIntent.ACTION_SEEK);
        intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, positionMillis);
        performItemAction(intent, mSessionId, itemId, extras, callback);
    }

    /**
     * Sends a request to get the status of a media item.
     * <p>
     * The request is issued in the current session.
     * </p><p>
     * Please refer to {@link MediaControlIntent#ACTION_GET_STATUS ACTION_GET_STATUS} for
     * more information about the semantics of this request.
     * </p>
     *
     * @param itemId The item id.
     * @param extras A bundle of extra arguments to be added to the
     * {@link MediaControlIntent#ACTION_GET_STATUS} intent, or null if none.
     * @param callback A callback to invoke when the request has been
     * processed, or null if none.
     *
     * @throws IllegalStateException if there is no current session.
     *
     * @see MediaControlIntent#ACTION_GET_STATUS
     * @see #isRemotePlaybackSupported
     */
    public void getStatus(@NonNull String itemId, @Nullable Bundle extras,
            @Nullable ItemActionCallback callback) {
        if (itemId == null) {
            throw new IllegalArgumentException("itemId must not be null");
        }
        throwIfNoCurrentSession();

        Intent intent = new Intent(MediaControlIntent.ACTION_GET_STATUS);
        performItemAction(intent, mSessionId, itemId, extras, callback);
    }

    /**
     * Sends a request to remove a media item from the queue.
     * <p>
     * The request is issued in the current session.
     * </p><p>
     * Please refer to {@link MediaControlIntent#ACTION_REMOVE ACTION_REMOVE} for
     * more information about the semantics of this request.
     * </p>
     *
     * @param itemId The item id.
     * @param extras A bundle of extra arguments to be added to the
     * {@link MediaControlIntent#ACTION_REMOVE} intent, or null if none.
     * @param callback A callback to invoke when the request has been
     * processed, or null if none.
     *
     * @throws IllegalStateException if there is no current session.
     * @throws UnsupportedOperationException if the route does not support queuing.
     *
     * @see MediaControlIntent#ACTION_REMOVE
     * @see #isRemotePlaybackSupported
     * @see #isQueuingSupported
     */
    public void remove(@NonNull String itemId, @Nullable Bundle extras,
            @Nullable ItemActionCallback callback) {
        if (itemId == null) {
            throw new IllegalArgumentException("itemId must not be null");
        }
        throwIfQueuingNotSupported();
        throwIfNoCurrentSession();

        Intent intent = new Intent(MediaControlIntent.ACTION_REMOVE);
        performItemAction(intent, mSessionId, itemId, extras, callback);
    }

    /**
     * Sends a request to pause media playback.
     * <p>
     * The request is issued in the current session.  If playback is already paused
     * then the request has no effect.
     * </p><p>
     * Please refer to {@link MediaControlIntent#ACTION_PAUSE ACTION_PAUSE} for
     * more information about the semantics of this request.
     * </p>
     *
     * @param extras A bundle of extra arguments to be added to the
     * {@link MediaControlIntent#ACTION_PAUSE} intent, or null if none.
     * @param callback A callback to invoke when the request has been
     * processed, or null if none.
     *
     * @throws IllegalStateException if there is no current session.
     *
     * @see MediaControlIntent#ACTION_PAUSE
     * @see #isRemotePlaybackSupported
     */
    public void pause(@Nullable Bundle extras, @Nullable SessionActionCallback callback) {
        throwIfNoCurrentSession();

        Intent intent = new Intent(MediaControlIntent.ACTION_PAUSE);
        performSessionAction(intent, mSessionId, extras, callback);
    }

    /**
     * Sends a request to resume (unpause) media playback.
     * <p>
     * The request is issued in the current session.  If playback is not paused
     * then the request has no effect.
     * </p><p>
     * Please refer to {@link MediaControlIntent#ACTION_RESUME ACTION_RESUME} for
     * more information about the semantics of this request.
     * </p>
     *
     * @param extras A bundle of extra arguments to be added to the
     * {@link MediaControlIntent#ACTION_RESUME} intent, or null if none.
     * @param callback A callback to invoke when the request has been
     * processed, or null if none.
     *
     * @throws IllegalStateException if there is no current session.
     *
     * @see MediaControlIntent#ACTION_RESUME
     * @see #isRemotePlaybackSupported
     */
    public void resume(@Nullable Bundle extras, @Nullable SessionActionCallback callback) {
        throwIfNoCurrentSession();

        Intent intent = new Intent(MediaControlIntent.ACTION_RESUME);
        performSessionAction(intent, mSessionId, extras, callback);
    }

    /**
     * Sends a request to stop media playback and clear the media playback queue.
     * <p>
     * The request is issued in the current session.  If the queue is already
     * empty then the request has no effect.
     * </p><p>
     * Please refer to {@link MediaControlIntent#ACTION_STOP ACTION_STOP} for
     * more information about the semantics of this request.
     * </p>
     *
     * @param extras A bundle of extra arguments to be added to the
     * {@link MediaControlIntent#ACTION_STOP} intent, or null if none.
     * @param callback A callback to invoke when the request has been
     * processed, or null if none.
     *
     * @throws IllegalStateException if there is no current session.
     *
     * @see MediaControlIntent#ACTION_STOP
     * @see #isRemotePlaybackSupported
     */
    public void stop(@Nullable Bundle extras, @Nullable SessionActionCallback callback) {
        throwIfNoCurrentSession();

        Intent intent = new Intent(MediaControlIntent.ACTION_STOP);
        performSessionAction(intent, mSessionId, extras, callback);
    }

    /**
     * Sends a request to start a new media playback session.
     * <p>
     * The application must wait for the callback to indicate that this request
     * is complete before issuing other requests that affect the session.  If this
     * request is successful then the previous session will be invalidated.
     * </p><p>
     * Please refer to {@link MediaControlIntent#ACTION_START_SESSION ACTION_START_SESSION}
     * for more information about the semantics of this request.
     * </p>
     *
     * @param extras A bundle of extra arguments to be added to the
     * {@link MediaControlIntent#ACTION_START_SESSION} intent, or null if none.
     * @param callback A callback to invoke when the request has been
     * processed, or null if none.
     *
     * @throws UnsupportedOperationException if the route does not support session management.
     *
     * @see MediaControlIntent#ACTION_START_SESSION
     * @see #isRemotePlaybackSupported
     * @see #isSessionManagementSupported
     */
    public void startSession(@Nullable Bundle extras, @Nullable SessionActionCallback callback) {
        throwIfSessionManagementNotSupported();

        Intent intent = new Intent(MediaControlIntent.ACTION_START_SESSION);
        intent.putExtra(MediaControlIntent.EXTRA_SESSION_STATUS_UPDATE_RECEIVER,
                mSessionStatusPendingIntent);
        if (mRouteSupportsMessaging) {
            intent.putExtra(MediaControlIntent.EXTRA_MESSAGE_RECEIVER, mMessagePendingIntent);
        }
        performSessionAction(intent, null, extras, callback);
    }

    /**
     * Sends a message.
     * <p>
     * The request is issued in the current session.
     * </p><p>
     * Please refer to {@link MediaControlIntent#ACTION_SEND_MESSAGE} for
     * more information about the semantics of this request.
     * </p>
     *
     * @param message A bundle message denoting {@link MediaControlIntent#EXTRA_MESSAGE}.
     * @param callback A callback to invoke when the request has been processed, or null if none.
     *
     * @throws IllegalStateException if there is no current session.
     * @throws UnsupportedOperationException if the route does not support messages.
     *
     * @see MediaControlIntent#ACTION_SEND_MESSAGE
     * @see #isMessagingSupported
     */
    public void sendMessage(@Nullable Bundle message, @Nullable SessionActionCallback callback) {
        throwIfNoCurrentSession();
        throwIfMessageNotSupported();

        Intent intent = new Intent(MediaControlIntent.ACTION_SEND_MESSAGE);
        performSessionAction(intent, mSessionId, message, callback);
    }

    /**
     * Sends a request to get the status of the media playback session.
     * <p>
     * The request is issued in the current session.
     * </p><p>
     * Please refer to {@link MediaControlIntent#ACTION_GET_SESSION_STATUS
     * ACTION_GET_SESSION_STATUS} for more information about the semantics of this request.
     * </p>
     *
     * @param extras A bundle of extra arguments to be added to the
     * {@link MediaControlIntent#ACTION_GET_SESSION_STATUS} intent, or null if none.
     * @param callback A callback to invoke when the request has been
     * processed, or null if none.
     *
     * @throws IllegalStateException if there is no current session.
     * @throws UnsupportedOperationException if the route does not support session management.
     *
     * @see MediaControlIntent#ACTION_GET_SESSION_STATUS
     * @see #isRemotePlaybackSupported
     * @see #isSessionManagementSupported
     */
    public void getSessionStatus(@Nullable Bundle extras,
            @Nullable SessionActionCallback callback) {
        throwIfSessionManagementNotSupported();
        throwIfNoCurrentSession();

        Intent intent = new Intent(MediaControlIntent.ACTION_GET_SESSION_STATUS);
        performSessionAction(intent, mSessionId, extras, callback);
    }

    /**
     * Sends a request to end the media playback session.
     * <p>
     * The request is issued in the current session.  If this request is successful,
     * the {@link #getSessionId session id property} will be set to null after
     * the callback is invoked.
     * </p><p>
     * Please refer to {@link MediaControlIntent#ACTION_END_SESSION ACTION_END_SESSION}
     * for more information about the semantics of this request.
     * </p>
     *
     * @param extras A bundle of extra arguments to be added to the
     * {@link MediaControlIntent#ACTION_END_SESSION} intent, or null if none.
     * @param callback A callback to invoke when the request has been
     * processed, or null if none.
     *
     * @throws IllegalStateException if there is no current session.
     * @throws UnsupportedOperationException if the route does not support session management.
     *
     * @see MediaControlIntent#ACTION_END_SESSION
     * @see #isRemotePlaybackSupported
     * @see #isSessionManagementSupported
     */
    public void endSession(@Nullable Bundle extras, @Nullable SessionActionCallback callback) {
        throwIfSessionManagementNotSupported();
        throwIfNoCurrentSession();

        Intent intent = new Intent(MediaControlIntent.ACTION_END_SESSION);
        performSessionAction(intent, mSessionId, extras, callback);
    }

    private void performItemAction(final Intent intent,
            final String sessionId, final String itemId,
            Bundle extras, final ItemActionCallback callback) {
        intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
        if (sessionId != null) {
            intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sessionId);
        }
        if (itemId != null) {
            intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, itemId);
        }
        if (extras != null) {
            intent.putExtras(extras);
        }
        logRequest(intent);
        mRoute.sendControlRequest(intent, new MediaRouter.ControlRequestCallback() {
            @Override
            public void onResult(Bundle data) {
                if (data != null) {
                    String sessionIdResult = inferMissingResult(sessionId,
                            data.getString(MediaControlIntent.EXTRA_SESSION_ID));
                    MediaSessionStatus sessionStatus = MediaSessionStatus.fromBundle(
                            data.getBundle(MediaControlIntent.EXTRA_SESSION_STATUS));
                    String itemIdResult = inferMissingResult(itemId,
                            data.getString(MediaControlIntent.EXTRA_ITEM_ID));
                    MediaItemStatus itemStatus = MediaItemStatus.fromBundle(
                            data.getBundle(MediaControlIntent.EXTRA_ITEM_STATUS));
                    adoptSession(sessionIdResult);
                    if (sessionIdResult != null && itemIdResult != null && itemStatus != null) {
                        if (DEBUG) {
                            Log.d(TAG, "Received result from " + intent.getAction()
                                    + ": data=" + bundleToString(data)
                                    + ", sessionId=" + sessionIdResult
                                    + ", sessionStatus=" + sessionStatus
                                    + ", itemId=" + itemIdResult
                                    + ", itemStatus=" + itemStatus);
                        }
                        callback.onResult(data, sessionIdResult, sessionStatus,
                                itemIdResult, itemStatus);
                        return;
                    }
                }
                handleInvalidResult(intent, callback, data);
            }

            @Override
            public void onError(String error, Bundle data) {
                handleError(intent, callback, error, data);
            }
        });
    }

    private void performSessionAction(final Intent intent, final String sessionId,
            Bundle extras, final SessionActionCallback callback) {
        intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
        if (sessionId != null) {
            intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sessionId);
        }
        if (extras != null) {
            intent.putExtras(extras);
        }
        logRequest(intent);
        mRoute.sendControlRequest(intent, new MediaRouter.ControlRequestCallback() {
            @Override
            public void onResult(Bundle data) {
                if (data != null) {
                    String sessionIdResult = inferMissingResult(sessionId,
                            data.getString(MediaControlIntent.EXTRA_SESSION_ID));
                    MediaSessionStatus sessionStatus = MediaSessionStatus.fromBundle(
                            data.getBundle(MediaControlIntent.EXTRA_SESSION_STATUS));
                    adoptSession(sessionIdResult);
                    if (sessionIdResult != null) {
                        if (DEBUG) {
                            Log.d(TAG, "Received result from " + intent.getAction()
                                    + ": data=" + bundleToString(data)
                                    + ", sessionId=" + sessionIdResult
                                    + ", sessionStatus=" + sessionStatus);
                        }
                        try {
                            callback.onResult(data, sessionIdResult, sessionStatus);
                        } finally {
                            if (intent.getAction().equals(MediaControlIntent.ACTION_END_SESSION)
                                    && sessionIdResult.equals(mSessionId)) {
                                setSessionId(null);
                            }
                        }
                        return;
                    }
                }
                handleInvalidResult(intent, callback, data);
            }

            @Override
            public void onError(String error, Bundle data) {
                handleError(intent, callback, error, data);
            }
        });
    }

    void adoptSession(String sessionId) {
        if (sessionId != null) {
            setSessionId(sessionId);
        }
    }

    void handleInvalidResult(Intent intent, ActionCallback callback,
            Bundle data) {
        Log.w(TAG, "Received invalid result data from " + intent.getAction()
                + ": data=" + bundleToString(data));
        callback.onError(null, MediaControlIntent.ERROR_UNKNOWN, data);
    }

    void handleError(Intent intent, ActionCallback callback,
            String error, Bundle data) {
        final int code;
        if (data != null) {
            code = data.getInt(MediaControlIntent.EXTRA_ERROR_CODE,
                    MediaControlIntent.ERROR_UNKNOWN);
        } else {
            code = MediaControlIntent.ERROR_UNKNOWN;
        }
        if (DEBUG) {
            Log.w(TAG, "Received error from " + intent.getAction()
                    + ": error=" + error
                    + ", code=" + code
                    + ", data=" + bundleToString(data));
        }
        callback.onError(error, code, data);
    }

    private void detectFeatures() {
        mRouteSupportsRemotePlayback = routeSupportsAction(MediaControlIntent.ACTION_PLAY)
                && routeSupportsAction(MediaControlIntent.ACTION_SEEK)
                && routeSupportsAction(MediaControlIntent.ACTION_GET_STATUS)
                && routeSupportsAction(MediaControlIntent.ACTION_PAUSE)
                && routeSupportsAction(MediaControlIntent.ACTION_RESUME)
                && routeSupportsAction(MediaControlIntent.ACTION_STOP);
        mRouteSupportsQueuing = mRouteSupportsRemotePlayback
                && routeSupportsAction(MediaControlIntent.ACTION_ENQUEUE)
                && routeSupportsAction(MediaControlIntent.ACTION_REMOVE);
        mRouteSupportsSessionManagement = mRouteSupportsRemotePlayback
                && routeSupportsAction(MediaControlIntent.ACTION_START_SESSION)
                && routeSupportsAction(MediaControlIntent.ACTION_GET_SESSION_STATUS)
                && routeSupportsAction(MediaControlIntent.ACTION_END_SESSION);
        mRouteSupportsMessaging = doesRouteSupportMessaging();
    }

    private boolean routeSupportsAction(String action) {
        return mRoute.supportsControlAction(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK, action);
    }

    private boolean doesRouteSupportMessaging() {
        for (IntentFilter filter : mRoute.getControlFilters()) {
            if (filter.hasAction(MediaControlIntent.ACTION_SEND_MESSAGE)) {
                return true;
            }
        }
        return false;
    }

    private void throwIfRemotePlaybackNotSupported() {
        if (!mRouteSupportsRemotePlayback) {
            throw new UnsupportedOperationException("The route does not support remote playback.");
        }
    }

    private void throwIfQueuingNotSupported() {
        if (!mRouteSupportsQueuing) {
            throw new UnsupportedOperationException("The route does not support queuing.");
        }
    }

    private void throwIfSessionManagementNotSupported() {
        if (!mRouteSupportsSessionManagement) {
            throw new UnsupportedOperationException("The route does not support "
                    + "session management.");
        }
    }

    private void throwIfMessageNotSupported() {
        if (!mRouteSupportsMessaging) {
            throw new UnsupportedOperationException("The route does not support message.");
        }
    }

    private void throwIfNoCurrentSession() {
        if (mSessionId == null) {
            throw new IllegalStateException("There is no current session.");
        }
    }

    static String inferMissingResult(String request, String result) {
        if (result == null) {
            // Result is missing.
            return request;
        }
        if (request == null || request.equals(result)) {
            // Request didn't specify a value or result matches request.
            return result;
        }
        // Result conflicts with request.
        return null;
    }

    private static void logRequest(Intent intent) {
        if (DEBUG) {
            Log.d(TAG, "Sending request: " + intent);
        }
    }

    static String bundleToString(Bundle bundle) {
        if (bundle != null) {
            bundle.size(); // force bundle to be unparcelled
            return bundle.toString();
        }
        return "null";
    }

    private final class ActionReceiver extends BroadcastReceiver {
        public static final String ACTION_ITEM_STATUS_CHANGED =
                "androidx.mediarouter.media.actions.ACTION_ITEM_STATUS_CHANGED";
        public static final String ACTION_SESSION_STATUS_CHANGED =
                "androidx.mediarouter.media.actions.ACTION_SESSION_STATUS_CHANGED";
        public static final String ACTION_MESSAGE_RECEIVED =
                "androidx.mediarouter.media.actions.ACTION_MESSAGE_RECEIVED";

        ActionReceiver() {
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            String sessionId = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
            if (sessionId == null || !sessionId.equals(mSessionId)) {
                Log.w(TAG, "Discarding spurious status callback "
                        + "with missing or invalid session id: sessionId=" + sessionId);
                return;
            }

            MediaSessionStatus sessionStatus = MediaSessionStatus.fromBundle(
                    intent.getBundleExtra(MediaControlIntent.EXTRA_SESSION_STATUS));
            String action = intent.getAction();
            if (action.equals(ACTION_ITEM_STATUS_CHANGED)) {
                String itemId = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
                if (itemId == null) {
                    Log.w(TAG, "Discarding spurious status callback with missing item id.");
                    return;
                }

                MediaItemStatus itemStatus = MediaItemStatus.fromBundle(
                        intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_STATUS));
                if (itemStatus == null) {
                    Log.w(TAG, "Discarding spurious status callback with missing item status.");
                    return;
                }

                if (DEBUG) {
                    Log.d(TAG, "Received item status callback: sessionId=" + sessionId
                            + ", sessionStatus=" + sessionStatus
                            + ", itemId=" + itemId
                            + ", itemStatus=" + itemStatus);
                }

                if (mStatusCallback != null) {
                    mStatusCallback.onItemStatusChanged(intent.getExtras(),
                            sessionId, sessionStatus, itemId, itemStatus);
                }
            } else if (action.equals(ACTION_SESSION_STATUS_CHANGED)) {
                if (sessionStatus == null) {
                    Log.w(TAG, "Discarding spurious media status callback with "
                            +"missing session status.");
                    return;
                }

                if (DEBUG) {
                    Log.d(TAG, "Received session status callback: sessionId=" + sessionId
                            + ", sessionStatus=" + sessionStatus);
                }

                if (mStatusCallback != null) {
                    mStatusCallback.onSessionStatusChanged(intent.getExtras(),
                            sessionId, sessionStatus);
                }
            } else if (action.equals(ACTION_MESSAGE_RECEIVED)) {
                if (DEBUG) {
                    Log.d(TAG, "Received message callback: sessionId=" + sessionId);
                }

                if (mOnMessageReceivedListener != null) {
                    mOnMessageReceivedListener.onMessageReceived(sessionId,
                            intent.getBundleExtra(MediaControlIntent.EXTRA_MESSAGE));
                }
            }
        }
    }

    /**
     * A callback that will receive media status updates.
     */
    public static abstract class StatusCallback {
        /**
         * Called when the status of a media item changes.
         *
         * @param data The result data bundle.
         * @param sessionId The session id.
         * @param sessionStatus The session status, or null if unknown.
         * @param itemId The item id.
         * @param itemStatus The item status.
         */
        public void onItemStatusChanged(@Nullable Bundle data,
                @NonNull String sessionId, @Nullable MediaSessionStatus sessionStatus,
                @NonNull String itemId, @NonNull MediaItemStatus itemStatus) {
        }

        /**
         * Called when the status of a media session changes.
         *
         * @param data The result data bundle.
         * @param sessionId The session id.
         * @param sessionStatus The session status, or null if unknown.
         */
        public void onSessionStatusChanged(@Nullable Bundle data,
                @NonNull String sessionId, @Nullable MediaSessionStatus sessionStatus) {
        }

        /**
         * Called when the session of the remote playback client changes.
         *
         * @param sessionId The new session id.
         */
        public void onSessionChanged(@Nullable String sessionId) {
        }
    }

    /**
     * Base callback type for remote playback requests.
     */
    public static abstract class ActionCallback {
        /**
         * Called when a media control request fails.
         *
         * @param error A localized error message which may be shown to the user, or null
         * if the cause of the error is unclear.
         * @param code The error code, or {@link MediaControlIntent#ERROR_UNKNOWN} if unknown.
         * @param data The error data bundle, or null if none.
         */
        public void onError(@Nullable String error, int code, @Nullable Bundle data) {
        }
    }

    /**
     * Callback for remote playback requests that operate on items.
     */
    public static abstract class ItemActionCallback extends ActionCallback {
        /**
         * Called when the request succeeds.
         *
         * @param data The result data bundle.
         * @param sessionId The session id.
         * @param sessionStatus The session status, or null if unknown.
         * @param itemId The item id.
         * @param itemStatus The item status.
         */
        public void onResult(@NonNull Bundle data, @NonNull String sessionId,
                @Nullable MediaSessionStatus sessionStatus,
                @NonNull String itemId, @NonNull MediaItemStatus itemStatus) {
        }
    }

    /**
     * Callback for remote playback requests that operate on sessions.
     */
    public static abstract class SessionActionCallback extends ActionCallback {
        /**
         * Called when the request succeeds.
         *
         * @param data The result data bundle.
         * @param sessionId The session id.
         * @param sessionStatus The session status, or null if unknown.
         */
        public void onResult(@NonNull Bundle data, @NonNull String sessionId,
                @Nullable MediaSessionStatus sessionStatus) {
        }
    }

    /**
     * A callback that will receive messages from media sessions.
     */
    public interface OnMessageReceivedListener {
        /**
         * Called when a message received.
         *
         * @param sessionId The session id.
         * @param message A bundle message denoting {@link MediaControlIntent#EXTRA_MESSAGE}.
         */
        void onMessageReceived(@NonNull String sessionId, @Nullable Bundle message);
    }
}