/*
* 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.content.ComponentName;
import android.content.Context;
import android.media.AudioManager;
import android.media.IRemoteVolumeController;
import android.media.session.ISessionManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.service.notification.NotificationListenerService;
import android.util.ArrayMap;
import android.util.Log;
import android.view.KeyEvent;
import java.util.ArrayList;
import java.util.List;
/**
* Provides support for interacting with {@link MediaSession media sessions}
* that applications have published to express their ongoing media playback
* state.
* <p>
* Use <code>Context.getSystemService(Context.MEDIA_SESSION_SERVICE)</code> to
* get an instance of this class.
*
* @see MediaSession
* @see MediaController
*/
public final class MediaSessionManager {
private static final String TAG = "SessionManager";
private final ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper> mListeners
= new ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper>();
private final Object mLock = new Object();
private final ISessionManager mService;
private Context mContext;
/**
* @hide
*/
public MediaSessionManager(Context context) {
// Consider rewriting like DisplayManagerGlobal
// Decide if we need context
mContext = context;
IBinder b = ServiceManager.getService(Context.MEDIA_SESSION_SERVICE);
mService = ISessionManager.Stub.asInterface(b);
}
/**
* Create a new session in the system and get the binder for it.
*
* @param tag A short name for debugging purposes.
* @return The binder object from the system
* @hide
*/
public @NonNull ISession createSession(@NonNull MediaSession.CallbackStub cbStub,
@NonNull String tag, int userId) throws RemoteException {
return mService.createSession(mContext.getPackageName(), cbStub, tag, userId);
}
/**
* Get a list of controllers for all ongoing sessions. The controllers will
* be provided in priority order with the most important controller at index
* 0.
* <p>
* This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL
* permission be held by the calling app. You may also retrieve this list if
* your app is an enabled notification listener using the
* {@link NotificationListenerService} APIs, in which case you must pass the
* {@link ComponentName} of your enabled listener.
*
* @param notificationListener The enabled notification listener component.
* May be null.
* @return A list of controllers for ongoing sessions.
*/
public @NonNull List<MediaController> getActiveSessions(
@Nullable ComponentName notificationListener) {
return getActiveSessionsForUser(notificationListener, UserHandle.myUserId());
}
/**
* Get active sessions for a specific user. To retrieve actions for a user
* other than your own you must hold the
* {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission
* in addition to any other requirements. If you are an enabled notification
* listener you may only get sessions for the users you are enabled for.
*
* @param notificationListener The enabled notification listener component.
* May be null.
* @param userId The user id to fetch sessions for.
* @return A list of controllers for ongoing sessions.
* @hide
*/
public @NonNull List<MediaController> getActiveSessionsForUser(
@Nullable ComponentName notificationListener, int userId) {
ArrayList<MediaController> controllers = new ArrayList<MediaController>();
try {
List<IBinder> binders = mService.getSessions(notificationListener, userId);
int size = binders.size();
for (int i = 0; i < size; i++) {
MediaController controller = new MediaController(mContext, ISessionController.Stub
.asInterface(binders.get(i)));
controllers.add(controller);
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to get active sessions: ", e);
}
return controllers;
}
/**
* Add a listener to be notified when the list of active sessions
* changes.This requires the
* android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by
* the calling app. You may also retrieve this list if your app is an
* enabled notification listener using the
* {@link NotificationListenerService} APIs, in which case you must pass the
* {@link ComponentName} of your enabled listener. Updates will be posted to
* the thread that registered the listener.
*
* @param sessionListener The listener to add.
* @param notificationListener The enabled notification listener component.
* May be null.
*/
public void addOnActiveSessionsChangedListener(
@NonNull OnActiveSessionsChangedListener sessionListener,
@Nullable ComponentName notificationListener) {
addOnActiveSessionsChangedListener(sessionListener, notificationListener, null);
}
/**
* Add a listener to be notified when the list of active sessions
* changes.This requires the
* android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by
* the calling app. You may also retrieve this list if your app is an
* enabled notification listener using the
* {@link NotificationListenerService} APIs, in which case you must pass the
* {@link ComponentName} of your enabled listener. Updates will be posted to
* the handler specified or to the caller's thread if the handler is null.
*
* @param sessionListener The listener to add.
* @param notificationListener The enabled notification listener component.
* May be null.
* @param handler The handler to post events to.
*/
public void addOnActiveSessionsChangedListener(
@NonNull OnActiveSessionsChangedListener sessionListener,
@Nullable ComponentName notificationListener, @Nullable Handler handler) {
addOnActiveSessionsChangedListener(sessionListener, notificationListener,
UserHandle.myUserId(), handler);
}
/**
* Add a listener to be notified when the list of active sessions
* changes.This requires the
* android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by
* the calling app. You may also retrieve this list if your app is an
* enabled notification listener using the
* {@link NotificationListenerService} APIs, in which case you must pass the
* {@link ComponentName} of your enabled listener.
*
* @param sessionListener The listener to add.
* @param notificationListener The enabled notification listener component.
* May be null.
* @param userId The userId to listen for changes on.
* @param handler The handler to post updates on.
* @hide
*/
public void addOnActiveSessionsChangedListener(
@NonNull OnActiveSessionsChangedListener sessionListener,
@Nullable ComponentName notificationListener, int userId, @Nullable Handler handler) {
if (sessionListener == null) {
throw new IllegalArgumentException("listener may not be null");
}
if (handler == null) {
handler = new Handler();
}
synchronized (mLock) {
if (mListeners.get(sessionListener) != null) {
Log.w(TAG, "Attempted to add session listener twice, ignoring.");
return;
}
SessionsChangedWrapper wrapper = new SessionsChangedWrapper(mContext, sessionListener,
handler);
try {
mService.addSessionsListener(wrapper.mStub, notificationListener, userId);
mListeners.put(sessionListener, wrapper);
} catch (RemoteException e) {
Log.e(TAG, "Error in addOnActiveSessionsChangedListener.", e);
}
}
}
/**
* Stop receiving active sessions updates on the specified listener.
*
* @param listener The listener to remove.
*/
public void removeOnActiveSessionsChangedListener(
@NonNull OnActiveSessionsChangedListener listener) {
if (listener == null) {
throw new IllegalArgumentException("listener may not be null");
}
synchronized (mLock) {
SessionsChangedWrapper wrapper = mListeners.remove(listener);
if (wrapper != null) {
try {
mService.removeSessionsListener(wrapper.mStub);
} catch (RemoteException e) {
Log.e(TAG, "Error in removeOnActiveSessionsChangedListener.", e);
} finally {
wrapper.release();
}
}
}
}
/**
* Set the remote volume controller to receive volume updates on. Only for
* use by system UI.
*
* @param rvc The volume controller to receive updates on.
* @hide
*/
public void setRemoteVolumeController(IRemoteVolumeController rvc) {
try {
mService.setRemoteVolumeController(rvc);
} catch (RemoteException e) {
Log.e(TAG, "Error in setRemoteVolumeController.", e);
}
}
/**
* Send a media key event. The receiver will be selected automatically.
*
* @param keyEvent The KeyEvent to send.
* @hide
*/
public void dispatchMediaKeyEvent(@NonNull KeyEvent keyEvent) {
dispatchMediaKeyEvent(keyEvent, false);
}
/**
* Send a media key event. The receiver will be selected automatically.
*
* @param keyEvent The KeyEvent to send.
* @param needWakeLock True if a wake lock should be held while sending the key.
* @hide
*/
public void dispatchMediaKeyEvent(@NonNull KeyEvent keyEvent, boolean needWakeLock) {
try {
mService.dispatchMediaKeyEvent(keyEvent, needWakeLock);
} catch (RemoteException e) {
Log.e(TAG, "Failed to send key event.", e);
}
}
/**
* Dispatch an adjust volume request to the system. It will be sent to the
* most relevant audio stream or media session. The direction must be one of
* {@link AudioManager#ADJUST_LOWER}, {@link AudioManager#ADJUST_RAISE},
* {@link AudioManager#ADJUST_SAME}.
*
* @param suggestedStream The stream to fall back to if there isn't a
* relevant stream
* @param direction The direction to adjust volume in.
* @param flags Any flags to include with the volume change.
* @hide
*/
public void dispatchAdjustVolume(int suggestedStream, int direction, int flags) {
try {
mService.dispatchAdjustVolume(suggestedStream, direction, flags);
} catch (RemoteException e) {
Log.e(TAG, "Failed to send adjust volume.", e);
}
}
/**
* Check if the global priority session is currently active. This can be
* used to decide if media keys should be sent to the session or to the app.
*
* @hide
*/
public boolean isGlobalPriorityActive() {
try {
return mService.isGlobalPriorityActive();
} catch (RemoteException e) {
Log.e(TAG, "Failed to check if the global priority is active.", e);
}
return false;
}
/**
* Listens for changes to the list of active sessions. This can be added
* using {@link #addOnActiveSessionsChangedListener}.
*/
public interface OnActiveSessionsChangedListener {
public void onActiveSessionsChanged(@Nullable List<MediaController> controllers);
}
private static final class SessionsChangedWrapper {
private Context mContext;
private OnActiveSessionsChangedListener mListener;
private Handler mHandler;
public SessionsChangedWrapper(Context context, OnActiveSessionsChangedListener listener,
Handler handler) {
mContext = context;
mListener = listener;
mHandler = handler;
}
private final IActiveSessionsListener.Stub mStub = new IActiveSessionsListener.Stub() {
@Override
public void onActiveSessionsChanged(final List<MediaSession.Token> tokens) {
if (mHandler != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
if (mListener != null) {
ArrayList<MediaController> controllers
= new ArrayList<MediaController>();
int size = tokens.size();
for (int i = 0; i < size; i++) {
controllers.add(new MediaController(mContext, tokens.get(i)));
}
mListener.onActiveSessionsChanged(controllers);
}
}
});
}
}
};
private void release() {
mContext = null;
mListener = null;
mHandler = null;
}
}
}