Introduce common interfaces for both session1 and session2

This is intermediate step toward for making session2 to handle media
key event.

Bug: 147279043
Test: Build and run media cts
Change-Id: I2a5723dab9e81db2338575774567f2e7b1d6f8c1
This commit is contained in:
Jaewan Kim
2019-12-30 14:55:30 +09:00
parent 2d3c66da00
commit 0ad953fc38
6 changed files with 391 additions and 89 deletions

View File

@@ -141,6 +141,9 @@ public class MediaController2 implements AutoCloseable {
// Note: unbindService() throws IllegalArgumentException when it's called twice.
return;
}
if (DEBUG) {
Log.d(TAG, "closing " + this);
}
mClosed = true;
if (mServiceConnection != null) {
// Note: This should be called even when the bindService() has returned false.

View File

@@ -0,0 +1,164 @@
/*
* Copyright 2019 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 com.android.server.media;
import android.media.MediaController2;
import android.media.Session2CommandGroup;
import android.media.Session2Token;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Looper;
import android.os.ResultReceiver;
import android.os.UserHandle;
import android.util.Log;
import android.view.KeyEvent;
import com.android.internal.annotations.GuardedBy;
import java.io.PrintWriter;
/**
* Keeps the record of {@link Session2Token} helps to send command to the corresponding session.
*/
// TODO(jaewan): Do not call service method directly -- introduce listener instead.
public class MediaSession2Record implements MediaSessionRecordImpl {
private static final String TAG = "MediaSession2Record";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final Object mLock = new Object();
@GuardedBy("mLock")
private final Session2Token mSessionToken;
@GuardedBy("mLock")
private final HandlerExecutor mHandlerExecutor;
@GuardedBy("mLock")
private final MediaController2 mController;
@GuardedBy("mLock")
private final MediaSessionService mService;
@GuardedBy("mLock")
private boolean mIsConnected;
public MediaSession2Record(Session2Token sessionToken, MediaSessionService service,
Looper handlerLooper) {
mSessionToken = sessionToken;
mService = service;
mHandlerExecutor = new HandlerExecutor(new Handler(handlerLooper));
mController = new MediaController2.Builder(service.getContext(), sessionToken)
.setControllerCallback(mHandlerExecutor, new Controller2Callback())
.build();
}
@Override
public String getPackageName() {
return mSessionToken.getPackageName();
}
public Session2Token getSession2Token() {
return mSessionToken;
}
@Override
public int getUid() {
return mSessionToken.getUid();
}
@Override
public int getUserId() {
return UserHandle.getUserId(mSessionToken.getUid());
}
@Override
public boolean isSystemPriority() {
// System priority session is currently only allowed for telephony, and it's OK to stick to
// the media1 API at this moment.
return false;
}
@Override
public void adjustVolume(String packageName, String opPackageName, int pid, int uid,
boolean asSystemService, int direction, int flags, boolean useSuggested) {
// TODO(jaewan): Add API to adjust volume.
}
@Override
public boolean isActive() {
synchronized (mLock) {
return mIsConnected;
}
}
@Override
public boolean checkPlaybackActiveState(boolean expected) {
synchronized (mLock) {
return mIsConnected && mController.isPlaybackActive() == expected;
}
}
@Override
public boolean isPlaybackTypeLocal() {
// TODO(jaewan): Implement -- need API to know whether the playback is remote or local.
return true;
}
@Override
public void close() {
synchronized (mLock) {
// Call close regardless of the mIsAvailable. This may be called when it's not yet
// connected.
mController.close();
}
}
@Override
public boolean sendMediaButton(String packageName, int pid, int uid, boolean asSystemService,
KeyEvent ke, int sequenceId, ResultReceiver cb) {
// TODO(jaewan): Implement.
return false;
}
@Override
public void dump(PrintWriter pw, String prefix) {
pw.println(prefix + "token=" + mSessionToken);
pw.println(prefix + "controller=" + mController);
final String indent = prefix + " ";
pw.println(indent + "playbackActive=" + mController.isPlaybackActive());
}
private class Controller2Callback extends MediaController2.ControllerCallback {
@Override
public void onConnected(MediaController2 controller, Session2CommandGroup allowedCommands) {
if (DEBUG) {
Log.d(TAG, "connected to " + mSessionToken + ", allowed=" + allowedCommands);
}
synchronized (mLock) {
mIsConnected = true;
}
mService.pushSession2TokensChanged(MediaSession2Record.this);
}
@Override
public void onDisconnected(MediaController2 controller) {
if (DEBUG) {
Log.d(TAG, "disconnected from " + mSessionToken);
}
synchronized (mLock) {
mIsConnected = false;
}
mService.sessionDied(MediaSession2Record.this);
}
}
}

View File

@@ -62,7 +62,8 @@ import java.util.List;
* This is the system implementation of a Session. Apps will interact with the
* MediaSession wrapper class instead.
*/
public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable {
// TODO(jaewan): Do not call service method directly -- introduce listener instead.
public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionRecordImpl {
private static final String TAG = "MediaSessionRecord";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -170,6 +171,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
*
* @return Info that identifies this session.
*/
@Override
public String getPackageName() {
return mPackageName;
}
@@ -188,6 +190,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
*
* @return The UID for this session.
*/
@Override
public int getUid() {
return mOwnerUid;
}
@@ -197,6 +200,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
*
* @return The user id for this session.
*/
@Override
public int getUserId() {
return mUserId;
}
@@ -207,6 +211,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
*
* @return True if this is a system priority session, false otherwise
*/
@Override
public boolean isSystemPriority() {
return (mFlags & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0;
}
@@ -318,9 +323,13 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
/**
* Check if this session has been set to active by the app.
* <p>
* It's not used to prioritize sessions for dispatching media keys since API 26, but still used
* to filter session list in MediaSessionManager#getActiveSessions().
*
* @return True if the session is active, false otherwise.
*/
@Override
public boolean isActive() {
return mIsActive && !mDestroyed;
}
@@ -333,6 +342,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
* @param expected True if playback is expected to be active. false otherwise.
* @return True if the session's playback matches with the expectation. false otherwise.
*/
@Override
public boolean checkPlaybackActiveState(boolean expected) {
if (mPlaybackState == null) {
return false;
@@ -345,7 +355,8 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
*
* @return {@code true} if the playback is local.
*/
public boolean isPlaybackLocal() {
@Override
public boolean isPlaybackTypeLocal() {
return mVolumeType == PlaybackInfo.PLAYBACK_TYPE_LOCAL;
}
@@ -383,7 +394,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
* @param sequenceId (optional) sequence id. Use this only when a wake lock is needed.
* @param cb (optional) result receiver to receive callback. Use this only when a wake lock is
* needed.
* @return {@code true} if the attempt to send media button was successfuly.
* @return {@code true} if the attempt to send media button was successfully.
* {@code false} otherwise.
*/
public boolean sendMediaButton(String packageName, int pid, int uid, boolean asSystemService,
@@ -392,6 +403,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
cb);
}
@Override
public void dump(PrintWriter pw, String prefix) {
pw.println(prefix + mTag + " " + this);
@@ -712,7 +724,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, AutoCloseable
public void destroySession() throws RemoteException {
final long token = Binder.clearCallingIdentity();
try {
mService.destroySession(MediaSessionRecord.this);
mService.sessionDied(MediaSessionRecord.this);
} finally {
Binder.restoreCallingIdentity(token);
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright 2019 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 com.android.server.media;
import android.media.AudioManager;
import android.os.ResultReceiver;
import android.view.KeyEvent;
import java.io.PrintWriter;
/**
* Common interfaces between {@link MediaSessionRecord} and {@link MediaSession2Record}.
*/
public interface MediaSessionRecordImpl extends AutoCloseable {
/**
* Get the info for this session.
*
* @return Info that identifies this session.
*/
String getPackageName();
/**
* Get the UID this session was created for.
*
* @return The UID for this session.
*/
int getUid();
/**
* Get the user id this session was created for.
*
* @return The user id for this session.
*/
int getUserId();
/**
* Check if this session has system priorty and should receive media buttons
* before any other sessions.
*
* @return True if this is a system priority session, false otherwise
*/
boolean isSystemPriority();
/**
* Send a volume adjustment to the session owner. Direction must be one of
* {@link AudioManager#ADJUST_LOWER}, {@link AudioManager#ADJUST_RAISE},
* {@link AudioManager#ADJUST_SAME}.
*
* @param packageName The package that made the original volume request.
* @param opPackageName The op package that made the original volume request.
* @param pid The pid that made the original volume request.
* @param uid The uid that made the original volume request.
* @param asSystemService {@code true} if the event sent to the session as if it was come from
* the system service instead of the app process. This helps sessions to distinguish
* between the key injection by the app and key events from the hardware devices.
* Should be used only when the volume key events aren't handled by foreground
* activity. {@code false} otherwise to tell session about the real caller.
* @param direction The direction to adjust volume in.
* @param flags Any of the flags from {@link AudioManager}.
* @param useSuggested True to use adjustSuggestedStreamVolume instead of
*/
void adjustVolume(String packageName, String opPackageName, int pid, int uid,
boolean asSystemService, int direction, int flags, boolean useSuggested);
/**
* Check if this session has been set to active by the app. (i.e. ready to receive command and
* getters are available).
*
* @return True if the session is active, false otherwise.
*/
// TODO(jaewan): Find better naming, or remove this from the MediaSessionRecordImpl.
boolean isActive();
/**
* Check if the session's playback active state matches with the expectation. This always return
* {@code false} if the playback state is unknown (e.g. {@code null}), where we cannot know the
* actual playback state associated with the session.
*
* @param expected True if playback is expected to be active. false otherwise.
* @return True if the session's playback matches with the expectation. false otherwise.
*/
boolean checkPlaybackActiveState(boolean expected);
/**
* Check whether the playback type is local or remote.
* <p>
* <ul>
* <li>Local: volume changes the stream volume because playback happens on this device.</li>
* <li>Remote: volume is sent to the apps callback because playback happens on the remote
* device and we cannot know how to control the volume of it.</li>
* </ul>
*
* @return {@code true} if the playback is local. {@code false} if the playback is remote.
*/
boolean isPlaybackTypeLocal();
/**
* Sends media button.
*
* @param packageName caller package name
* @param pid caller pid
* @param uid caller uid
* @param asSystemService {@code true} if the event sent to the session as if it was come from
* the system service instead of the app process.
* @param ke key events
* @param sequenceId (optional) sequence id. Use this only when a wake lock is needed.
* @param cb (optional) result receiver to receive callback. Use this only when a wake lock is
* needed.
* @return {@code true} if the attempt to send media button was successfully.
* {@code false} otherwise.
*/
boolean sendMediaButton(String packageName, int pid, int uid, boolean asSystemService,
KeyEvent ke, int sequenceId, ResultReceiver cb);
/**
* Dumps internal state
*
* @param pw print writer
* @param prefix prefix
*/
void dump(PrintWriter pw, String prefix);
/**
* Override {@link AutoCloseable#close} to tell not to throw exception.
*/
@Override
void close();
}

View File

@@ -40,10 +40,7 @@ import android.media.AudioManager;
import android.media.AudioManagerInternal;
import android.media.AudioPlaybackConfiguration;
import android.media.AudioSystem;
import android.media.IAudioService;
import android.media.IRemoteVolumeController;
import android.media.MediaController2;
import android.media.Session2CommandGroup;
import android.media.Session2Token;
import android.media.session.IActiveSessionsListener;
import android.media.session.IOnMediaKeyEventDispatchedListener;
@@ -61,7 +58,6 @@ import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
@@ -127,7 +123,8 @@ public class MediaSessionService extends SystemService implements Monitor {
// TODO: Keep session2 info in MediaSessionStack for prioritizing both session1 and session2 in
// one place.
@GuardedBy("mLock")
private final SparseArray<List<Session2Token>> mSession2TokensPerUser = new SparseArray<>();
private final SparseArray<List<MediaSession2Record>> mSession2RecordsPerUser =
new SparseArray<>();
@GuardedBy("mLock")
private final List<Session2TokensListenerRecord> mSession2TokensListenerRecords =
new ArrayList<>();
@@ -189,11 +186,6 @@ public class MediaSessionService extends SystemService implements Monitor {
updateUser();
}
private IAudioService getAudioService() {
IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
return IAudioService.Stub.asInterface(b);
}
private boolean isGlobalPriorityActiveLocked() {
return mGlobalPrioritySession != null && mGlobalPrioritySession.isActive();
}
@@ -266,11 +258,21 @@ public class MediaSessionService extends SystemService implements Monitor {
List<Session2Token> getSession2TokensLocked(int userId) {
List<Session2Token> list = new ArrayList<>();
if (userId == USER_ALL) {
for (int i = 0; i < mSession2TokensPerUser.size(); i++) {
list.addAll(mSession2TokensPerUser.valueAt(i));
for (int i = 0; i < mSession2RecordsPerUser.size(); i++) {
List<MediaSession2Record> records = mSession2RecordsPerUser.valueAt(i);
for (MediaSession2Record record: records) {
if (record.isActive()) {
list.add(record.getSession2Token());
}
}
}
} else {
list.addAll(mSession2TokensPerUser.get(userId));
List<MediaSession2Record> records = mSession2RecordsPerUser.get(userId);
for (MediaSession2Record record: records) {
if (record.isActive()) {
list.add(record.getSession2Token());
}
}
}
return list;
}
@@ -347,7 +349,13 @@ public class MediaSessionService extends SystemService implements Monitor {
user.destroySessionsForUserLocked(userId);
}
}
mSession2TokensPerUser.remove(userId);
List<MediaSession2Record> list = mSession2RecordsPerUser.get(userId);
if (list != null) {
for (MediaSession2Record session : list) {
session.close();
}
mSession2RecordsPerUser.remove(userId);
}
updateUser();
}
}
@@ -372,9 +380,17 @@ public class MediaSessionService extends SystemService implements Monitor {
}
}
void destroySession(MediaSessionRecord session) {
void pushSession2TokensChanged(MediaSession2Record sessionRecord) {
synchronized (mLock) {
destroySessionLocked(session);
pushSession2TokensChangedLocked(sessionRecord.getUserId());
}
}
void sessionDied(MediaSession2Record sessionRecord) {
synchronized (mLock) {
int userId = sessionRecord.getUserId();
mSession2RecordsPerUser.get(userId).remove(sessionRecord);
pushSession2TokensChangedLocked(userId);
}
}
@@ -393,8 +409,8 @@ public class MediaSessionService extends SystemService implements Monitor {
mUserRecords.put(userInfo.id, new FullUserRecord(userInfo.id));
}
}
if (mSession2TokensPerUser.get(userInfo.id) == null) {
mSession2TokensPerUser.put(userInfo.id, new ArrayList<>());
if (mSession2RecordsPerUser.get(userInfo.id) == null) {
mSession2RecordsPerUser.put(userInfo.id, new ArrayList<>());
}
}
}
@@ -405,8 +421,8 @@ public class MediaSessionService extends SystemService implements Monitor {
Log.w(TAG, "Cannot find FullUserInfo for the current user " + currentFullUserId);
mCurrentFullUserRecord = new FullUserRecord(currentFullUserId);
mUserRecords.put(currentFullUserId, mCurrentFullUserRecord);
if (mSession2TokensPerUser.get(currentFullUserId) == null) {
mSession2TokensPerUser.put(currentFullUserId, new ArrayList<>());
if (mSession2RecordsPerUser.get(currentFullUserId) == null) {
mSession2RecordsPerUser.put(currentFullUserId, new ArrayList<>());
}
}
mFullUserIds.put(currentFullUserId, currentFullUserId);
@@ -541,15 +557,6 @@ public class MediaSessionService extends SystemService implements Monitor {
return false;
}
private MediaSessionRecord createSessionInternal(int callerPid, int callerUid, int userId,
String callerPackageName, ISessionCallback cb, String tag, Bundle sessionInfo)
throws RemoteException {
synchronized (mLock) {
return createSessionLocked(callerPid, callerUid, userId, callerPackageName, cb,
tag, sessionInfo);
}
}
/*
* When a session is created the following things need to happen.
* 1. Its callback binder needs a link to death
@@ -557,29 +564,31 @@ public class MediaSessionService extends SystemService implements Monitor {
* 3. It needs to be added to the priority stack.
* 4. It needs to be added to the relevant user record.
*/
private MediaSessionRecord createSessionLocked(int callerPid, int callerUid, int userId,
private MediaSessionRecord createSessionInternal(int callerPid, int callerUid, int userId,
String callerPackageName, ISessionCallback cb, String tag, Bundle sessionInfo) {
FullUserRecord user = getFullUserRecordLocked(userId);
if (user == null) {
Log.w(TAG, "Request from invalid user: " + userId + ", pkg=" + callerPackageName);
throw new RuntimeException("Session request from invalid user.");
}
synchronized (mLock) {
FullUserRecord user = getFullUserRecordLocked(userId);
if (user == null) {
Log.w(TAG, "Request from invalid user: " + userId + ", pkg=" + callerPackageName);
throw new RuntimeException("Session request from invalid user.");
}
final MediaSessionRecord session;
try {
session = new MediaSessionRecord(callerPid, callerUid, userId,
callerPackageName, cb, tag, sessionInfo, this, mHandler.getLooper());
} catch (RemoteException e) {
throw new RuntimeException("Media Session owner died prematurely.", e);
}
final MediaSessionRecord session;
try {
session = new MediaSessionRecord(callerPid, callerUid, userId,
callerPackageName, cb, tag, sessionInfo, this, mHandler.getLooper());
} catch (RemoteException e) {
throw new RuntimeException("Media Session owner died prematurely.", e);
}
user.mPriorityStack.addSession(session);
mHandler.postSessionsChanged(userId);
user.mPriorityStack.addSession(session);
mHandler.postSessionsChanged(userId);
if (DEBUG) {
Log.d(TAG, "Created session for " + callerPackageName + " with tag " + tag);
if (DEBUG) {
Log.d(TAG, "Created session for " + callerPackageName + " with tag " + tag);
}
return session;
}
return session;
}
private int findIndexOfSessionsListenerLocked(IActiveSessionsListener listener) {
@@ -869,13 +878,13 @@ public class MediaSessionService extends SystemService implements Monitor {
+ mRestoredMediaButtonReceiverComponentType);
mPriorityStack.dump(pw, indent);
pw.println(indent + "Session2Tokens:");
for (int i = 0; i < mSession2TokensPerUser.size(); i++) {
List<Session2Token> list = mSession2TokensPerUser.valueAt(i);
for (int i = 0; i < mSession2RecordsPerUser.size(); i++) {
List<MediaSession2Record> list = mSession2RecordsPerUser.valueAt(i);
if (list == null || list.size() == 0) {
continue;
}
for (Session2Token token : list) {
pw.println(indent + " " + token);
for (MediaSession2Record record : list) {
record.dump(pw, indent);
}
}
}
@@ -1132,14 +1141,10 @@ public class MediaSessionService extends SystemService implements Monitor {
throw new SecurityException("Unexpected Session2Token's UID, expected=" + uid
+ " but actually=" + sessionToken.getUid());
}
Controller2Callback callback = new Controller2Callback(sessionToken);
// Note: It's safe not to keep controller here because it wouldn't be GC'ed until
// it's closed.
// TODO: Keep controller as well for better readability
// because the GC behavior isn't straightforward.
MediaController2 controller = new MediaController2.Builder(mContext, sessionToken)
.setControllerCallback(new HandlerExecutor(mHandler), callback)
.build();
MediaSession2Record record = new MediaSession2Record(
sessionToken, MediaSessionService.this, mHandler.getLooper());
mSession2RecordsPerUser.get(record.getUserId()).add(record);
// Do not immediately notify changes -- do so when framework can dispatch command
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -2424,29 +2429,4 @@ public class MediaSessionService extends SystemService implements Monitor {
}
}
private class Controller2Callback extends MediaController2.ControllerCallback {
private final Session2Token mToken;
Controller2Callback(Session2Token token) {
mToken = token;
}
@Override
public void onConnected(MediaController2 controller, Session2CommandGroup allowedCommands) {
synchronized (mLock) {
int userId = UserHandle.getUserId(mToken.getUid());
mSession2TokensPerUser.get(userId).add(mToken);
pushSession2TokensChangedLocked(userId);
}
}
@Override
public void onDisconnected(MediaController2 controller) {
synchronized (mLock) {
int userId = UserHandle.getUserId(mToken.getUid());
mSession2TokensPerUser.get(userId).remove(mToken);
pushSession2TokensChangedLocked(userId);
}
}
}
}

View File

@@ -300,7 +300,7 @@ class MediaSessionStack {
int size = records.size();
for (int i = 0; i < size; i++) {
MediaSessionRecord record = records.get(i);
if (!record.isPlaybackLocal()) {
if (!record.isPlaybackTypeLocal()) {
return record;
}
}