/*
* Copyright (C) 2014 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 android.media.session;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ParceledListSlice;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.MediaMetadata;
import android.media.Rating;
import android.media.VolumeProvider;
import android.media.routing.MediaRouter;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* Allows interaction with media controllers, volume keys, media buttons, and
* transport controls.
*
* A MediaSession should be created when an app wants to publish media playback
* information or handle media keys. In general an app only needs one session
* for all playback, though multiple sessions can be created to provide finer
* grain controls of media.
*
* Once a session is created the owner of the session may pass its
* {@link #getSessionToken() session token} to other processes to allow them to
* create a {@link MediaController} to interact with the session.
*
* To receive commands, media keys, and other events a {@link Callback} must be
* set with {@link #addCallback(Callback)} and {@link #setActive(boolean)
* setActive(true)} must be called. To receive transport control commands a
* {@link TransportControlsCallback} must be set with
* {@link #addTransportControlsCallback}.
*
* When an app is finished performing playback it must call {@link #release()}
* to clean up the session and notify any controllers.
*
* MediaSession objects are thread safe.
*/
public final class MediaSession {
private static final String TAG = "MediaSession";
/**
* Set this flag on the session to indicate that it can handle media button
* events.
*/
public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0;
/**
* Set this flag on the session to indicate that it handles transport
* control commands through a {@link TransportControlsCallback}.
* The callback can be retrieved by calling {@link #addTransportControlsCallback}.
*/
public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1;
/**
* System only flag for a session that needs to have priority over all other
* sessions. This flag ensures this session will receive media button events
* regardless of the current ordering in the system.
*
* @hide
*/
public static final int FLAG_EXCLUSIVE_GLOBAL_PRIORITY = 1 << 16;
/**
* The session uses local playback.
*/
public static final int PLAYBACK_TYPE_LOCAL = 1;
/**
* The session uses remote playback.
*/
public static final int PLAYBACK_TYPE_REMOTE = 2;
private final Object mLock = new Object();
private final MediaSession.Token mSessionToken;
private final ISession mBinder;
private final CallbackStub mCbStub;
private final ArrayList mCallbacks
= new ArrayList();
private final ArrayList mTransportCallbacks
= new ArrayList();
private VolumeProvider mVolumeProvider;
private boolean mActive = false;
/**
* Creates a new session. The session will automatically be registered with
* the system but will not be published until {@link #setActive(boolean)
* setActive(true)} is called. You must call {@link #release()} when
* finished with the session.
*
* @param context The context to use to create the session.
* @param tag A short name for debugging purposes.
*/
public MediaSession(@NonNull Context context, @NonNull String tag) {
this(context, tag, UserHandle.myUserId());
}
/**
* Creates a new session as the specified user. To create a session as a
* user other than your own you must hold the
* {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL}
* permission.
*
* @param context The context to use to create the session.
* @param tag A short name for debugging purposes.
* @param userId The user id to create the session as.
* @hide
*/
public MediaSession(@NonNull Context context, @NonNull String tag, int userId) {
if (context == null) {
throw new IllegalArgumentException("context cannot be null.");
}
if (TextUtils.isEmpty(tag)) {
throw new IllegalArgumentException("tag cannot be null or empty");
}
mCbStub = new CallbackStub(this);
MediaSessionManager manager = (MediaSessionManager) context
.getSystemService(Context.MEDIA_SESSION_SERVICE);
try {
mBinder = manager.createSession(mCbStub, tag, userId);
mSessionToken = new Token(mBinder.getController());
} catch (RemoteException e) {
throw new RuntimeException("Remote error creating session.", e);
}
}
/**
* Add a callback to receive updates on for the MediaSession. This includes
* media button and volume events. The caller's thread will be used to post
* events.
*
* @param callback The callback object
*/
public void addCallback(@NonNull Callback callback) {
addCallback(callback, null);
}
/**
* Add a callback to receive updates for the MediaSession. This includes
* media button and volume events.
*
* @param callback The callback to receive updates on.
* @param handler The handler that events should be posted on.
*/
public void addCallback(@NonNull Callback callback, @Nullable Handler handler) {
if (callback == null) {
throw new IllegalArgumentException("Callback cannot be null");
}
synchronized (mLock) {
if (getHandlerForCallbackLocked(callback) != null) {
Log.w(TAG, "Callback is already added, ignoring");
return;
}
if (handler == null) {
handler = new Handler();
}
CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(),
callback);
mCallbacks.add(msgHandler);
}
}
/**
* Remove a callback. It will no longer receive updates.
*
* @param callback The callback to remove.
*/
public void removeCallback(@NonNull Callback callback) {
synchronized (mLock) {
removeCallbackLocked(callback);
}
}
/**
* Set an intent for launching UI for this Session. This can be used as a
* quick link to an ongoing media screen.
*
* @param pi The intent to launch to show UI for this Session.
*/
public void setLaunchPendingIntent(@Nullable PendingIntent pi) {
// TODO
}
/**
* Associates a {@link MediaRouter} with this session to control the destination
* of media content.
*
* A media router may only be associated with at most one session at a time.
*
*
* @param router The media router, or null to remove the current association.
*/
public void setMediaRouter(@Nullable MediaRouter router) {
try {
mBinder.setMediaRouter(router != null ? router.getBinder() : null);
} catch (RemoteException e) {
Log.wtf(TAG, "Failure in setMediaButtonReceiver.", e);
}
}
/**
* Set a media button event receiver component to use to restart playback
* after an app has been stopped.
*
* @param mbr The receiver component to send the media button event to.
* @hide
*/
public void setMediaButtonReceiver(@Nullable ComponentName mbr) {
try {
mBinder.setMediaButtonReceiver(mbr);
} catch (RemoteException e) {
Log.wtf(TAG, "Failure in setMediaButtonReceiver.", e);
}
}
/**
* Set any flags for the session.
*
* @param flags The flags to set for this session.
*/
public void setFlags(int flags) {
try {
mBinder.setFlags(flags);
} catch (RemoteException e) {
Log.wtf(TAG, "Failure in setFlags.", e);
}
}
/**
* Set the attributes for this session's audio. This will affect the
* system's volume handling for this session. If
* {@link #setPlaybackToRemote} was previously called it will stop receiving
* volume commands and the system will begin sending volume changes to the
* appropriate stream.
*
* By default sessions use attributes for media.
*
* @param attributes The {@link AudioAttributes} for this session's audio.
*/
public void setPlaybackToLocal(AudioAttributes attributes) {
if (attributes == null) {
throw new IllegalArgumentException("Attributes cannot be null for local playback.");
}
try {
mBinder.setPlaybackToLocal(attributes);
} catch (RemoteException e) {
Log.wtf(TAG, "Failure in setPlaybackToLocal.", e);
}
}
/**
* Configure this session to use remote volume handling. This must be called
* to receive volume button events, otherwise the system will adjust the
* appropriate stream volume for this session. If
* {@link #setPlaybackToLocal} was previously called the system will stop
* handling volume changes for this session and pass them to the volume
* provider instead.
*
* @param volumeProvider The provider that will handle volume changes. May
* not be null.
*/
public void setPlaybackToRemote(@NonNull VolumeProvider volumeProvider) {
if (volumeProvider == null) {
throw new IllegalArgumentException("volumeProvider may not be null!");
}
mVolumeProvider = volumeProvider;
volumeProvider.setCallback(new VolumeProvider.Callback() {
@Override
public void onVolumeChanged(VolumeProvider volumeProvider) {
notifyRemoteVolumeChanged(volumeProvider);
}
});
try {
mBinder.setPlaybackToRemote(volumeProvider.getVolumeControl(),
volumeProvider.getMaxVolume());
} catch (RemoteException e) {
Log.wtf(TAG, "Failure in setPlaybackToRemote.", e);
}
}
/**
* Set if this session is currently active and ready to receive commands. If
* set to false your session's controller may not be discoverable. You must
* set the session to active before it can start receiving media button
* events or transport commands.
*
* @param active Whether this session is active or not.
*/
public void setActive(boolean active) {
if (mActive == active) {
return;
}
try {
mBinder.setActive(active);
mActive = active;
} catch (RemoteException e) {
Log.wtf(TAG, "Failure in setActive.", e);
}
}
/**
* Get the current active state of this session.
*
* @return True if the session is active, false otherwise.
*/
public boolean isActive() {
return mActive;
}
/**
* Send a proprietary event to all MediaControllers listening to this
* Session. It's up to the Controller/Session owner to determine the meaning
* of any events.
*
* @param event The name of the event to send
* @param extras Any extras included with the event
*/
public void sendSessionEvent(@NonNull String event, @Nullable Bundle extras) {
if (TextUtils.isEmpty(event)) {
throw new IllegalArgumentException("event cannot be null or empty");
}
try {
mBinder.sendEvent(event, extras);
} catch (RemoteException e) {
Log.wtf(TAG, "Error sending event", e);
}
}
/**
* This must be called when an app has finished performing playback. If
* playback is expected to start again shortly the session can be left open,
* but it must be released if your activity or service is being destroyed.
*/
public void release() {
try {
mBinder.destroy();
} catch (RemoteException e) {
Log.wtf(TAG, "Error releasing session: ", e);
}
}
/**
* Retrieve a token object that can be used by apps to create a
* {@link MediaController} for interacting with this session. The owner of
* the session is responsible for deciding how to distribute these tokens.
*
* @return A token that can be used to create a MediaController for this
* session
*/
public @NonNull Token getSessionToken() {
return mSessionToken;
}
/**
* Add a callback to receive transport controls on, such as play, rewind, or
* fast forward.
*
* @param callback The callback object
*/
public void addTransportControlsCallback(@NonNull TransportControlsCallback callback) {
addTransportControlsCallback(callback, null);
}
/**
* Add a callback to receive transport controls on, such as play, rewind, or
* fast forward. The updates will be posted to the specified handler. If no
* handler is provided they will be posted to the caller's thread.
*
* @param callback The callback to receive updates on
* @param handler The handler to post the updates on
*/
public void addTransportControlsCallback(@NonNull TransportControlsCallback callback,
@Nullable Handler handler) {
if (callback == null) {
throw new IllegalArgumentException("Callback cannot be null");
}
synchronized (mLock) {
if (getTransportControlsHandlerForCallbackLocked(callback) != null) {
Log.w(TAG, "Callback is already added, ignoring");
return;
}
if (handler == null) {
handler = new Handler();
}
TransportMessageHandler msgHandler = new TransportMessageHandler(handler.getLooper(),
callback);
mTransportCallbacks.add(msgHandler);
}
}
/**
* Stop receiving transport controls on the specified callback. If an update
* has already been posted you may still receive it after this call returns.
*
* @param callback The callback to stop receiving updates on
*/
public void removeTransportControlsCallback(@NonNull TransportControlsCallback callback) {
if (callback == null) {
throw new IllegalArgumentException("Callback cannot be null");
}
synchronized (mLock) {
removeTransportControlsCallbackLocked(callback);
}
}
/**
* Update the current playback state.
*
* @param state The current state of playback
*/
public void setPlaybackState(@Nullable PlaybackState state) {
try {
mBinder.setPlaybackState(state);
} catch (RemoteException e) {
Log.wtf(TAG, "Dead object in setPlaybackState.", e);
}
}
/**
* Update the current metadata. New metadata can be created using
* {@link android.media.MediaMetadata.Builder}.
*
* @param metadata The new metadata
*/
public void setMetadata(@Nullable MediaMetadata metadata) {
try {
mBinder.setMetadata(metadata);
} catch (RemoteException e) {
Log.wtf(TAG, "Dead object in setPlaybackState.", e);
}
}
/**
* Update the list of tracks in the play queue. It is an ordered list and should contain the
* current track, and previous or upcoming tracks if they exist.
* Specify null if there is no current play queue.
*
* The queue should be of reasonable size. If the play queue is unbounded within your
* app, it is better to send a reasonable amount in a sliding window instead.
*
* @param queue A list of tracks in the play queue.
*/
public void setQueue(@Nullable List