/* * 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.Context; 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.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.ResultReceiver; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; /** * Allows an app to interact with an ongoing media session. Media buttons and * other commands can be sent to the session. A callback may be registered to * receive updates from the session, such as metadata and play state changes. *
* A MediaController can be created through {@link MediaSessionManager} if you * hold the "android.permission.MEDIA_CONTENT_CONTROL" permission or are an * enabled notification listener or by getting a {@link MediaSession.Token} * directly from the session owner. *
* MediaController objects are thread-safe.
*/
public final class MediaController {
private static final String TAG = "MediaController";
private static final int MSG_EVENT = 1;
private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
private static final int MSG_UPDATE_METADATA = 3;
private static final int MSG_UPDATE_VOLUME = 4;
private static final int MSG_UPDATE_QUEUE = 5;
private static final int MSG_UPDATE_QUEUE_TITLE = 6;
private static final int MSG_UPDATE_EXTRAS = 7;
private static final int MSG_DESTROYED = 8;
private final ISessionController mSessionBinder;
private final MediaSession.Token mToken;
private final Context mContext;
private final CallbackStub mCbStub = new CallbackStub(this);
private final ArrayList
*
*
* @return The supported rating type
*/
public int getRatingType() {
try {
return mSessionBinder.getRatingType();
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling getRatingType.", e);
return Rating.RATING_NONE;
}
}
/**
* Get the flags for this session. Flags are defined in {@link MediaSession}.
*
* @return The current set of flags for the session.
*/
public @MediaSession.SessionFlags long getFlags() {
try {
return mSessionBinder.getFlags();
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling getFlags.", e);
}
return 0;
}
/**
* Get the current playback info for this session.
*
* @return The current playback info or null.
*/
public @Nullable PlaybackInfo getPlaybackInfo() {
try {
ParcelableVolumeInfo result = mSessionBinder.getVolumeAttributes();
return new PlaybackInfo(result.volumeType, result.audioAttrs, result.controlType,
result.maxVolume, result.currentVolume);
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling getAudioInfo.", e);
}
return null;
}
/**
* Get an intent for launching UI associated with this session if one
* exists.
*
* @return A {@link PendingIntent} to launch UI or null.
*/
public @Nullable PendingIntent getSessionActivity() {
try {
return mSessionBinder.getLaunchPendingIntent();
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling getPendingIntent.", e);
}
return null;
}
/**
* Get the token for the session this is connected to.
*
* @return The token for the connected session.
*/
public @NonNull MediaSession.Token getSessionToken() {
return mToken;
}
/**
* Set the volume of the output this session is playing on. The command will
* be ignored if it does not support
* {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}. The flags in
* {@link AudioManager} may be used to affect the handling.
*
* @see #getPlaybackInfo()
* @param value The value to set it to, between 0 and the reported max.
* @param flags Flags from {@link AudioManager} to include with the volume
* request.
*/
public void setVolumeTo(int value, int flags) {
try {
mSessionBinder.setVolumeTo(value, flags, mContext.getPackageName());
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling setVolumeTo.", e);
}
}
/**
* Adjust the volume of the output this session is playing on. The direction
* must be one of {@link AudioManager#ADJUST_LOWER},
* {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
* The command will be ignored if the session does not support
* {@link VolumeProvider#VOLUME_CONTROL_RELATIVE} or
* {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}. The flags in
* {@link AudioManager} may be used to affect the handling.
*
* @see #getPlaybackInfo()
* @param direction The direction to adjust the volume in.
* @param flags Any flags to pass with the command.
*/
public void adjustVolume(int direction, int flags) {
try {
mSessionBinder.adjustVolume(direction, flags, mContext.getPackageName());
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling adjustVolumeBy.", e);
}
}
/**
* Registers a callback to receive updates from the Session. Updates will be
* posted on the caller's thread.
*
* @param callback The callback object, must not be null.
*/
public void registerCallback(@NonNull Callback callback) {
registerCallback(callback, null);
}
/**
* Registers a callback to receive updates from the session. Updates will be
* posted on the specified handler's thread.
*
* @param callback The callback object, must not be null.
* @param handler The handler to post updates on. If null the callers thread
* will be used.
*/
public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
if (handler == null) {
handler = new Handler();
}
synchronized (mLock) {
addCallbackLocked(callback, handler);
}
}
/**
* Unregisters the specified callback. If an update has already been posted
* you may still receive it after calling this method.
*
* @param callback The callback to remove.
*/
public void unregisterCallback(@NonNull Callback callback) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
synchronized (mLock) {
removeCallbackLocked(callback);
}
}
/**
* Sends a generic command to the session. It is up to the session creator
* to decide what commands and parameters they will support. As such,
* commands should only be sent to sessions that the controller owns.
*
* @param command The command to send
* @param args Any parameters to include with the command
* @param cb The callback to receive the result on
*/
public void sendCommand(@NonNull String command, @Nullable Bundle args,
@Nullable ResultReceiver cb) {
if (TextUtils.isEmpty(command)) {
throw new IllegalArgumentException("command cannot be null or empty");
}
try {
mSessionBinder.sendCommand(command, args, cb);
} catch (RemoteException e) {
Log.d(TAG, "Dead object in sendCommand.", e);
}
}
/**
* Get the session owner's package name.
*
* @return The package name of of the session owner.
*/
public String getPackageName() {
if (mPackageName == null) {
try {
mPackageName = mSessionBinder.getPackageName();
} catch (RemoteException e) {
Log.d(TAG, "Dead object in getPackageName.", e);
}
}
return mPackageName;
}
/**
* Get the session's tag for debugging purposes.
*
* @return The session's tag.
* @hide
*/
public String getTag() {
if (mTag == null) {
try {
mTag = mSessionBinder.getTag();
} catch (RemoteException e) {
Log.d(TAG, "Dead object in getTag.", e);
}
}
return mTag;
}
/*
* @hide
*/
ISessionController getSessionBinder() {
return mSessionBinder;
}
/**
* @hide
*/
public boolean controlsSameSession(MediaController other) {
if (other == null) return false;
return mSessionBinder.asBinder() == other.getSessionBinder().asBinder();
}
private void addCallbackLocked(Callback cb, Handler handler) {
if (getHandlerForCallbackLocked(cb) != null) {
Log.w(TAG, "Callback is already added, ignoring");
return;
}
MessageHandler holder = new MessageHandler(handler.getLooper(), cb);
mCallbacks.add(holder);
if (!mCbRegistered) {
try {
mSessionBinder.registerCallbackListener(mCbStub);
mCbRegistered = true;
} catch (RemoteException e) {
Log.e(TAG, "Dead object in registerCallback", e);
}
}
}
private boolean removeCallbackLocked(Callback cb) {
boolean success = false;
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
MessageHandler handler = mCallbacks.get(i);
if (cb == handler.mCallback) {
mCallbacks.remove(i);
success = true;
}
}
if (mCbRegistered && mCallbacks.size() == 0) {
try {
mSessionBinder.unregisterCallbackListener(mCbStub);
} catch (RemoteException e) {
Log.e(TAG, "Dead object in removeCallbackLocked");
}
mCbRegistered = false;
}
return success;
}
private MessageHandler getHandlerForCallbackLocked(Callback cb) {
if (cb == null) {
throw new IllegalArgumentException("Callback cannot be null");
}
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
MessageHandler handler = mCallbacks.get(i);
if (cb == handler.mCallback) {
return handler;
}
}
return null;
}
private final void postMessage(int what, Object obj, Bundle data) {
synchronized (mLock) {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
mCallbacks.get(i).post(what, obj, data);
}
}
}
/**
* Callback for receiving updates on from the session. A Callback can be
* registered using {@link #registerCallback}
*/
public static abstract class Callback {
/**
* Override to handle the session being destroyed. The session is no
* longer valid after this call and calls to it will be ignored.
*/
public void onSessionDestroyed() {
}
/**
* Override to handle custom events sent by the session owner without a
* specified interface. Controllers should only handle these for
* sessions they own.
*
* @param event The event from the session.
* @param extras Optional parameters for the event, may be null.
*/
public void onSessionEvent(@NonNull String event, @Nullable Bundle extras) {
}
/**
* Override to handle changes in playback state.
*
* @param state The new playback state of the session
*/
public void onPlaybackStateChanged(@NonNull PlaybackState state) {
}
/**
* Override to handle changes to the current metadata.
*
* @param metadata The current metadata for the session or null if none.
* @see MediaMetadata
*/
public void onMetadataChanged(@Nullable MediaMetadata metadata) {
}
/**
* Override to handle changes to items in the queue.
*
* @param queue A list of items in the current play queue. It should
* include the currently playing item as well as previous and
* upcoming items if applicable.
* @see MediaSession.QueueItem
*/
public void onQueueChanged(@Nullable List
*
*
* @return The type of playback this session is using.
*/
public int getPlaybackType() {
return mVolumeType;
}
/**
* Get the audio attributes for this session. The attributes will affect
* volume handling for the session. When the volume type is
* {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} these may be ignored by the
* remote volume handler.
*
* @return The attributes for this session.
*/
public AudioAttributes getAudioAttributes() {
return mAudioAttrs;
}
/**
* Get the type of volume control that can be used. One of:
*
*
*
* @return The type of volume control that may be used with this
* session.
*/
public int getVolumeControl() {
return mVolumeControl;
}
/**
* Get the maximum volume that may be set for this session.
*
* @return The maximum allowed volume where this session is playing.
*/
public int getMaxVolume() {
return mMaxVolume;
}
/**
* Get the current volume for this session.
*
* @return The current volume where this session is playing.
*/
public int getCurrentVolume() {
return mCurrentVolume;
}
}
private final static class CallbackStub extends ISessionControllerCallback.Stub {
private final WeakReference