Adds listeners for changes to the list of active sessions
The listeners get notified when sessions are added, removed, or
reprioritized.
Change-Id: I7f3bfc84049719c3b9c19016c6bac92e1a5c3179
diff --git a/Android.mk b/Android.mk
index a1d91c36..8b0e556 100644
--- a/Android.mk
+++ b/Android.mk
@@ -306,14 +306,15 @@
media/java/android/media/IRemoteVolumeObserver.aidl \
media/java/android/media/IRingtonePlayer.aidl \
media/java/android/media/IVolumeController.aidl \
- media/java/android/media/routeprovider/IRouteConnection.aidl \
- media/java/android/media/routeprovider/IRouteProvider.aidl \
- media/java/android/media/routeprovider/IRouteProviderCallback.aidl \
- media/java/android/media/session/ISessionController.aidl \
- media/java/android/media/session/ISessionControllerCallback.aidl \
- media/java/android/media/session/ISession.aidl \
- media/java/android/media/session/ISessionCallback.aidl \
- media/java/android/media/session/ISessionManager.aidl \
+ media/java/android/media/routeprovider/IRouteConnection.aidl \
+ media/java/android/media/routeprovider/IRouteProvider.aidl \
+ media/java/android/media/routeprovider/IRouteProviderCallback.aidl \
+ media/java/android/media/session/IActiveSessionsListener.aidl \
+ media/java/android/media/session/ISessionController.aidl \
+ media/java/android/media/session/ISessionControllerCallback.aidl \
+ media/java/android/media/session/ISession.aidl \
+ media/java/android/media/session/ISessionCallback.aidl \
+ media/java/android/media/session/ISessionManager.aidl \
media/java/android/media/tv/ITvInputClient.aidl \
media/java/android/media/tv/ITvInputHardware.aidl \
media/java/android/media/tv/ITvInputHardwareCallback.aidl \
diff --git a/media/java/android/media/session/IActiveSessionsListener.aidl b/media/java/android/media/session/IActiveSessionsListener.aidl
new file mode 100644
index 0000000..e5e24bc
--- /dev/null
+++ b/media/java/android/media/session/IActiveSessionsListener.aidl
@@ -0,0 +1,26 @@
+/* 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.media.session.MediaSessionToken;
+
+/**
+ * Listens for changes to the list of active sessions.
+ * @hide
+ */
+oneway interface IActiveSessionsListener {
+ void onActiveSessionsChanged(in List<MediaSessionToken> sessions);
+}
\ No newline at end of file
diff --git a/media/java/android/media/session/ISessionManager.aidl b/media/java/android/media/session/ISessionManager.aidl
index 6d9888f..bd1fa85 100644
--- a/media/java/android/media/session/ISessionManager.aidl
+++ b/media/java/android/media/session/ISessionManager.aidl
@@ -16,6 +16,7 @@
package android.media.session;
import android.content.ComponentName;
+import android.media.session.IActiveSessionsListener;
import android.media.session.ISession;
import android.media.session.ISessionCallback;
import android.os.Bundle;
@@ -30,4 +31,7 @@
List<IBinder> getSessions(in ComponentName compName, int userId);
void dispatchMediaKeyEvent(in KeyEvent keyEvent, boolean needWakeLock);
void dispatchAdjustVolumeBy(int suggestedStream, int delta, int flags);
+ void addSessionsListener(in IActiveSessionsListener listener, in ComponentName compName,
+ int userId);
+ void removeSessionsListener(in IActiveSessionsListener listener);
}
\ No newline at end of file
diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java
index 9e8b0d3..291bfc8 100644
--- a/media/java/android/media/session/MediaSessionManager.java
+++ b/media/java/android/media/session/MediaSessionManager.java
@@ -142,6 +142,50 @@
}
/**
+ * 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.
+ * @hide
+ */
+ public void addActiveSessionsListener(SessionListener sessionListener,
+ ComponentName notificationListener, int userId) {
+ if (sessionListener == null) {
+ throw new IllegalArgumentException("listener may not be null");
+ }
+ try {
+ mService.addSessionsListener(sessionListener.mStub, notificationListener, userId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error in addActiveSessionsListener.", e);
+ }
+ }
+
+ /**
+ * Stop receiving active sessions updates on the specified listener.
+ *
+ * @param listener The listener to remove.
+ * @hide
+ */
+ public void removeActiveSessionsListener(SessionListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener may not be null");
+ }
+ try {
+ mService.removeSessionsListener(listener.mStub);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error in removeActiveSessionsListener.", e);
+ }
+ }
+
+ /**
* Send a media key event. The receiver will be selected automatically.
*
* @param keyEvent The KeyEvent to send.
@@ -184,4 +228,35 @@
Log.e(TAG, "Failed to send adjust volume.", e);
}
}
+
+ /**
+ * Listens for changes to the list of active sessions. This can be added
+ * using {@link #addActiveSessionsListener}.
+ *
+ * @hide
+ */
+ public static abstract class SessionListener {
+ /**
+ * Called when the list of active sessions has changed. This can be due
+ * to a session being added or removed or the order of sessions
+ * changing.
+ *
+ * @param controllers The updated list of controllers for the user that
+ * changed.
+ */
+ public abstract void onActiveSessionsChanged(List<MediaController> controllers);
+
+ private final IActiveSessionsListener.Stub mStub = new IActiveSessionsListener.Stub() {
+ @Override
+ public void onActiveSessionsChanged(List<MediaSessionToken> tokens)
+ throws RemoteException {
+ ArrayList<MediaController> controllers = new ArrayList<MediaController>();
+ int size = tokens.size();
+ for (int i = 0; i < size; i++) {
+ controllers.add(MediaController.fromToken(tokens.get(i)));
+ }
+ SessionListener.this.onActiveSessionsChanged(controllers);
+ }
+ };
+ }
}
diff --git a/media/java/android/media/session/MediaSessionToken.java b/media/java/android/media/session/MediaSessionToken.java
index 86f5662..e599189 100644
--- a/media/java/android/media/session/MediaSessionToken.java
+++ b/media/java/android/media/session/MediaSessionToken.java
@@ -31,7 +31,7 @@
/**
* @hide
*/
- MediaSessionToken(ISessionController binder) {
+ public MediaSessionToken(ISessionController binder) {
mBinder = binder;
}
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 87665e1..4d1829d 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -29,9 +29,11 @@
import android.media.AudioManager;
import android.media.IAudioService;
import android.media.routeprovider.RouteRequest;
+import android.media.session.IActiveSessionsListener;
import android.media.session.ISession;
import android.media.session.ISessionCallback;
import android.media.session.ISessionManager;
+import android.media.session.MediaSessionToken;
import android.media.session.RouteInfo;
import android.media.session.RouteOptions;
import android.media.session.MediaSession;
@@ -39,6 +41,7 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
+import android.os.Message;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ResultReceiver;
@@ -75,10 +78,12 @@
private final ArrayList<MediaSessionRecord> mAllSessions = new ArrayList<MediaSessionRecord>();
private final SparseArray<UserRecord> mUserRecords = new SparseArray<UserRecord>();
+ private final ArrayList<SessionsListenerRecord> mSessionsListeners
+ = new ArrayList<SessionsListenerRecord>();
// private final ArrayList<MediaRouteProviderProxy> mProviders
// = new ArrayList<MediaRouteProviderProxy>();
private final Object mLock = new Object();
- private final Handler mHandler = new Handler();
+ private final MessageHandler mHandler = new MessageHandler();
private final PowerManager.WakeLock mMediaEventWakeLock;
private KeyguardManager mKeyguardManager;
@@ -200,15 +205,20 @@
}
}
}
+ mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, record.getUserId(), 0);
}
public void onSessionPlaystateChange(MediaSessionRecord record, int oldState, int newState) {
+ boolean updateSessions = false;
synchronized (mLock) {
if (!mAllSessions.contains(record)) {
Log.d(TAG, "Unknown session changed playback state. Ignoring.");
return;
}
- mPriorityStack.onPlaystateChange(record, oldState, newState);
+ updateSessions = mPriorityStack.onPlaystateChange(record, oldState, newState);
+ }
+ if (updateSessions) {
+ mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, record.getUserId(), 0);
}
}
@@ -315,6 +325,8 @@
// ignore exceptions while destroying a session.
}
session.onDestroy();
+
+ mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, session.getUserId(), 0);
}
private void enforcePackageName(String packageName, int uid) {
@@ -428,6 +440,8 @@
UserRecord user = getOrCreateUser(userId);
user.addSessionLocked(session);
+ mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, userId, 0);
+
if (DEBUG) {
Log.d(TAG, "Created session for package " + callerPackageName + " with tag " + tag);
}
@@ -453,11 +467,43 @@
return -1;
}
+ private int findIndexOfSessionsListenerLocked(IActiveSessionsListener listener) {
+ for (int i = mSessionsListeners.size() - 1; i >= 0; i--) {
+ if (mSessionsListeners.get(i).mListener == listener) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
private boolean isSessionDiscoverable(MediaSessionRecord record) {
// TODO probably want to check more than if it's active.
return record.isActive();
}
+ private void pushSessionsChanged(int userId) {
+ synchronized (mLock) {
+ List<MediaSessionRecord> records = mPriorityStack.getActiveSessions(userId);
+ int size = records.size();
+ ArrayList<MediaSessionToken> tokens = new ArrayList<MediaSessionToken>();
+ for (int i = 0; i < size; i++) {
+ tokens.add(new MediaSessionToken(records.get(i).getControllerBinder()));
+ }
+ for (int i = mSessionsListeners.size() - 1; i >= 0; i--) {
+ SessionsListenerRecord record = mSessionsListeners.get(i);
+ if (record.mUserId == UserHandle.USER_ALL || record.mUserId == userId) {
+ try {
+ record.mListener.onActiveSessionsChanged(tokens);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Dead ActiveSessionsListener in pushSessionsChanged, removing",
+ e);
+ mSessionsListeners.remove(i);
+ }
+ }
+ }
+ }
+ }
+
private MediaRouteProviderProxy.RoutesListener mRoutesCallback
= new MediaRouteProviderProxy.RoutesListener() {
@Override
@@ -613,6 +659,23 @@
};
}
+ final class SessionsListenerRecord implements IBinder.DeathRecipient {
+ private final IActiveSessionsListener mListener;
+ private final int mUserId;
+
+ public SessionsListenerRecord(IActiveSessionsListener listener, int userId) {
+ mListener = listener;
+ mUserId = userId;
+ }
+
+ @Override
+ public void binderDied() {
+ synchronized (mLock) {
+ mSessionsListeners.remove(this);
+ }
+ }
+ }
+
class SessionManagerImpl extends ISessionManager.Stub {
private static final String EXTRA_WAKELOCK_ACQUIRED =
"android.media.AudioService.WAKELOCK_ACQUIRED";
@@ -648,20 +711,7 @@
final long token = Binder.clearCallingIdentity();
try {
- String packageName = null;
- if (componentName != null) {
- // If they gave us a component name verify they own the
- // package
- packageName = componentName.getPackageName();
- enforcePackageName(packageName, uid);
- }
- // Check that they can make calls on behalf of the user and
- // get the final user id
- int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
- true /* allowAll */, true /* requireFull */, "getSessions", packageName);
- // Check if they have the permissions or their component is
- // enabled for the user they're calling from.
- enforceMediaPermissions(componentName, pid, uid, resolvedUserId);
+ int resolvedUserId = verifySessionsRequest(componentName, userId, pid, uid);
ArrayList<IBinder> binders = new ArrayList<IBinder>();
synchronized (mLock) {
ArrayList<MediaSessionRecord> records = mPriorityStack
@@ -677,6 +727,52 @@
}
}
+ @Override
+ public void addSessionsListener(IActiveSessionsListener listener,
+ ComponentName componentName, int userId) throws RemoteException {
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+
+ try {
+ int resolvedUserId = verifySessionsRequest(componentName, userId, pid, uid);
+ synchronized (mLock) {
+ int index = findIndexOfSessionsListenerLocked(listener);
+ if (index != -1) {
+ Log.w(TAG, "ActiveSessionsListener is already added, ignoring");
+ return;
+ }
+ SessionsListenerRecord record = new SessionsListenerRecord(listener,
+ resolvedUserId);
+ try {
+ listener.asBinder().linkToDeath(record, 0);
+ } catch (RemoteException e) {
+ Log.e(TAG, "ActiveSessionsListener is dead, ignoring it", e);
+ return;
+ }
+ mSessionsListeners.add(record);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void removeSessionsListener(IActiveSessionsListener listener)
+ throws RemoteException {
+ synchronized (mLock) {
+ int index = findIndexOfSessionsListenerLocked(listener);
+ if (index != -1) {
+ SessionsListenerRecord record = mSessionsListeners.remove(index);
+ try {
+ record.mListener.asBinder().unlinkToDeath(record, 0);
+ } catch (Exception e) {
+ // ignore exceptions, the record is being removed
+ }
+ }
+ }
+ }
+
/**
* Handles the dispatching of the media button events to one of the
* registered listeners, or if there was none, broadcast an
@@ -764,6 +860,25 @@
}
}
+ private int verifySessionsRequest(ComponentName componentName, int userId, final int pid,
+ final int uid) {
+ String packageName = null;
+ if (componentName != null) {
+ // If they gave us a component name verify they own the
+ // package
+ packageName = componentName.getPackageName();
+ enforcePackageName(packageName, uid);
+ }
+ // Check that they can make calls on behalf of the user and
+ // get the final user id
+ int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
+ true /* allowAll */, true /* requireFull */, "getSessions", packageName);
+ // Check if they have the permissions or their component is
+ // enabled for the user they're calling from.
+ enforceMediaPermissions(componentName, pid, uid, resolvedUserId);
+ return resolvedUserId;
+ }
+
private void dispatchAdjustVolumeByLocked(int suggestedStream, int delta, int flags,
MediaSessionRecord session) {
int direction = 0;
@@ -994,4 +1109,20 @@
};
}
+ final class MessageHandler extends Handler {
+ private static final int MSG_SESSIONS_CHANGED = 1;
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_SESSIONS_CHANGED:
+ pushSessionsChanged(msg.arg1);
+ break;
+ }
+ }
+
+ public void post(int what, int arg1, int arg2) {
+ obtainMessage(what, arg1, arg2).sendToTarget();
+ }
+ }
}
diff --git a/services/core/java/com/android/server/media/MediaSessionStack.java b/services/core/java/com/android/server/media/MediaSessionStack.java
index 803dee2..144ccfa 100644
--- a/services/core/java/com/android/server/media/MediaSessionStack.java
+++ b/services/core/java/com/android/server/media/MediaSessionStack.java
@@ -88,16 +88,19 @@
* @param record The record that changed.
* @param oldState Its old playback state.
* @param newState Its new playback state.
+ * @return true if the priority order was updated, false otherwise.
*/
- public void onPlaystateChange(MediaSessionRecord record, int oldState, int newState) {
+ public boolean onPlaystateChange(MediaSessionRecord record, int oldState, int newState) {
if (shouldUpdatePriority(oldState, newState)) {
mSessions.remove(record);
mSessions.add(0, record);
clearCache();
+ return true;
} else if (newState == PlaybackState.STATE_PAUSED) {
// Just clear the volume cache in this case
mCachedVolumeDefault = null;
}
+ return false;
}
/**