MediaSession2: Fix timing issue
Session/Controller needs mProvider. However, if the createProvider() interacts with other components, than other components may use session /controller object before mProvider is set. This CL prevents such issues by calling initialize() to communicate with other components after the provider is set. Test: Run all MediaComponents test once Change-Id: I7f4c52136038a0522471015344881552b678a2ab
This commit is contained in:
@@ -254,12 +254,13 @@ public class MediaController2 implements AutoCloseable {
|
||||
@NonNull @CallbackExecutor Executor executor, @NonNull ControllerCallback callback) {
|
||||
super();
|
||||
|
||||
mProvider = createProvider(context, token, executor, callback);
|
||||
// This also connects to the token.
|
||||
// Explicit connect() isn't added on purpose because retrying connect() is impossible with
|
||||
// session whose session binder is only valid while it's active.
|
||||
// prevent a controller from reusable after the
|
||||
// session is released and recreated.
|
||||
mProvider = createProvider(context, token, executor, callback);
|
||||
mProvider.initialize();
|
||||
}
|
||||
|
||||
MediaController2Provider createProvider(@NonNull Context context,
|
||||
|
||||
@@ -1005,8 +1005,8 @@ public class MediaSession2 implements AutoCloseable {
|
||||
Executor callbackExecutor, SessionCallback callback) {
|
||||
super();
|
||||
mProvider = createProvider(context, player, id, volumeProvider, ratingType, sessionActivity,
|
||||
callbackExecutor, callback
|
||||
);
|
||||
callbackExecutor, callback);
|
||||
mProvider.initialize();
|
||||
}
|
||||
|
||||
MediaSession2Provider createProvider(Context context, MediaPlayerInterface player, String id,
|
||||
|
||||
@@ -19,10 +19,13 @@ package android.media;
|
||||
import android.annotation.IntDef;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.SystemApi;
|
||||
import android.content.Context;
|
||||
import android.media.session.MediaSessionManager;
|
||||
import android.media.update.ApiLoader;
|
||||
import android.media.update.SessionToken2Provider;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.text.TextUtils;
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@@ -37,7 +40,6 @@ import java.lang.annotation.RetentionPolicy;
|
||||
* It can be also obtained by {@link MediaSessionManager}.
|
||||
* @hide
|
||||
*/
|
||||
// TODO(jaewan): Move Token to updatable!
|
||||
public final class SessionToken2 {
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE, TYPE_LIBRARY_SERVICE})
|
||||
@@ -48,91 +50,81 @@ public final class SessionToken2 {
|
||||
public static final int TYPE_SESSION_SERVICE = 1;
|
||||
public static final int TYPE_LIBRARY_SERVICE = 2;
|
||||
|
||||
private static final String KEY_TYPE = "android.media.token.type";
|
||||
private static final String KEY_PACKAGE_NAME = "android.media.token.package_name";
|
||||
private static final String KEY_SERVICE_NAME = "android.media.token.service_name";
|
||||
private static final String KEY_ID = "android.media.token.id";
|
||||
private static final String KEY_SESSION_BINDER = "android.media.token.session_binder";
|
||||
private final SessionToken2Provider mProvider;
|
||||
|
||||
private final @TokenType int mType;
|
||||
private final String mPackageName;
|
||||
private final String mServiceName;
|
||||
private final String mId;
|
||||
private final IMediaSession2 mSessionBinder;
|
||||
/**
|
||||
* Constructor for the token. You can only create token for session service or library service
|
||||
* to use by {@link MediaController2} or {@link MediaBrowser2}.
|
||||
*
|
||||
* @param context context
|
||||
* @param type type
|
||||
* @param packageName package name
|
||||
* @param serviceName name of service. Can be {@code null} if it's not an service.
|
||||
*/
|
||||
public SessionToken2(@NonNull Context context, @TokenType int type, @NonNull String packageName,
|
||||
@NonNull String serviceName) {
|
||||
this(context, -1, type, packageName, serviceName, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for the token.
|
||||
*
|
||||
* @hide
|
||||
* @param context context
|
||||
* @param uid uid
|
||||
* @param type type
|
||||
* @param packageName package name
|
||||
* @param id id
|
||||
* @param serviceName name of service. Can be {@code null} if it's not an service.
|
||||
* @param sessionBinder binder for this session. Can be {@code null} if it's service.
|
||||
* @hide
|
||||
* @param id id. Can be {@code null} if serviceName is specified.
|
||||
* @param sessionBinderInterface sessionBinder. Required for the session.
|
||||
*/
|
||||
// TODO(jaewan): UID is also needed.
|
||||
// TODO(jaewan): Unhide
|
||||
public SessionToken2(@TokenType int type, @NonNull String packageName, @NonNull String id,
|
||||
@Nullable String serviceName, @Nullable IMediaSession2 sessionBinder) {
|
||||
// TODO(jaewan): Add sanity check.
|
||||
mType = type;
|
||||
mPackageName = packageName;
|
||||
mId = id;
|
||||
mServiceName = serviceName;
|
||||
mSessionBinder = sessionBinder;
|
||||
@SystemApi
|
||||
public SessionToken2(@NonNull Context context, int uid, @TokenType int type,
|
||||
@NonNull String packageName, @Nullable String serviceName, @Nullable String id,
|
||||
@Nullable IInterface sessionBinderInterface) {
|
||||
mProvider = ApiLoader.getProvider(context)
|
||||
.createSessionToken2(context, this, uid, type, packageName,
|
||||
serviceName, id, sessionBinderInterface);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
return mType
|
||||
+ prime * (mPackageName.hashCode()
|
||||
+ prime * (mId.hashCode()
|
||||
+ prime * ((mServiceName != null ? mServiceName.hashCode() : 0)
|
||||
+ prime * (mSessionBinder != null ? mSessionBinder.asBinder().hashCode() : 0))));
|
||||
return mProvider.hashCode_impl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
SessionToken2 other = (SessionToken2) obj;
|
||||
if (!mPackageName.equals(other.getPackageName())
|
||||
|| !mServiceName.equals(other.getServiceName())
|
||||
|| !mId.equals(other.getId())
|
||||
|| mType != other.getType()) {
|
||||
return false;
|
||||
}
|
||||
if (mSessionBinder == other.getSessionBinder()) {
|
||||
return true;
|
||||
} else if (mSessionBinder == null || other.getSessionBinder() == null) {
|
||||
return false;
|
||||
}
|
||||
return mSessionBinder.asBinder().equals(other.getSessionBinder().asBinder());
|
||||
return mProvider.equals_impl(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SessionToken {pkg=" + mPackageName + " id=" + mId + " type=" + mType
|
||||
+ " service=" + mServiceName + " binder=" + mSessionBinder + "}";
|
||||
return mProvider.toString_impl();
|
||||
}
|
||||
|
||||
@SystemApi
|
||||
public SessionToken2Provider getProvider() {
|
||||
return mProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return uid of the session
|
||||
*/
|
||||
public int getUid() {
|
||||
return mProvider.getUid_impl();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return package name
|
||||
*/
|
||||
public String getPackageName() {
|
||||
return mPackageName;
|
||||
return mProvider.getPackageName_impl();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return id
|
||||
*/
|
||||
public String getId() {
|
||||
return mId;
|
||||
return mProvider.getId_imp();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,82 +133,23 @@ public final class SessionToken2 {
|
||||
* @see #TYPE_SESSION_SERVICE
|
||||
*/
|
||||
public @TokenType int getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return session binder.
|
||||
* @hide
|
||||
*/
|
||||
public @Nullable IMediaSession2 getSessionBinder() {
|
||||
return mSessionBinder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return service name if it's session service.
|
||||
* @hide
|
||||
*/
|
||||
public @Nullable String getServiceName() {
|
||||
return mServiceName;
|
||||
return mProvider.getType_impl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a token from the bundle, exported by {@link #toBundle()}.
|
||||
*
|
||||
* @param bundle
|
||||
* @return
|
||||
*/
|
||||
public static SessionToken2 fromBundle(@NonNull Bundle bundle) {
|
||||
if (bundle == null) {
|
||||
return null;
|
||||
}
|
||||
final @TokenType int type = bundle.getInt(KEY_TYPE, -1);
|
||||
final String packageName = bundle.getString(KEY_PACKAGE_NAME);
|
||||
final String serviceName = bundle.getString(KEY_SERVICE_NAME);
|
||||
final String id = bundle.getString(KEY_ID);
|
||||
final IBinder sessionBinder = bundle.getBinder(KEY_SESSION_BINDER);
|
||||
|
||||
// Sanity check.
|
||||
switch (type) {
|
||||
case TYPE_SESSION:
|
||||
if (!(sessionBinder instanceof IMediaSession2)) {
|
||||
throw new IllegalArgumentException("Session needs sessionBinder");
|
||||
}
|
||||
break;
|
||||
case TYPE_SESSION_SERVICE:
|
||||
if (TextUtils.isEmpty(serviceName)) {
|
||||
throw new IllegalArgumentException("Session service needs service name");
|
||||
}
|
||||
if (sessionBinder != null && !(sessionBinder instanceof IMediaSession2)) {
|
||||
throw new IllegalArgumentException("Invalid session binder");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid type");
|
||||
}
|
||||
if (TextUtils.isEmpty(packageName) || id == null) {
|
||||
throw new IllegalArgumentException("Package name nor ID cannot be null.");
|
||||
}
|
||||
// TODO(jaewan): Revisit here when we add connection callback to the session for individual
|
||||
// controller's permission check. With it, sessionBinder should be available
|
||||
// if and only if for session, not session service.
|
||||
return new SessionToken2(type, packageName, id, serviceName,
|
||||
sessionBinder != null ? IMediaSession2.Stub.asInterface(sessionBinder) : null);
|
||||
public static SessionToken2 fromBundle(@NonNull Context context, @NonNull Bundle bundle) {
|
||||
return ApiLoader.getProvider(context).SessionToken2_fromBundle(context, bundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link Bundle} from this token to share it across processes.
|
||||
*
|
||||
* @return Bundle
|
||||
*/
|
||||
public Bundle toBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(KEY_PACKAGE_NAME, mPackageName);
|
||||
bundle.putString(KEY_SERVICE_NAME, mServiceName);
|
||||
bundle.putString(KEY_ID, mId);
|
||||
bundle.putInt(KEY_TYPE, mType);
|
||||
bundle.putBinder(KEY_SESSION_BINDER,
|
||||
mSessionBinder != null ? mSessionBinder.asBinder() : null);
|
||||
return bundle;
|
||||
return mProvider.toBundle_impl();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ interface ISessionManager {
|
||||
void setOnMediaKeyListener(in IOnMediaKeyListener listener);
|
||||
|
||||
// MediaSession2
|
||||
Bundle createSessionToken(String callingPackage, String id, IMediaSession2 binder);
|
||||
boolean onSessionCreated(in Bundle sessionToken);
|
||||
void onSessionDestroyed(in Bundle sessionToken);
|
||||
List<Bundle> getSessionTokens(boolean activeSessionOnly, boolean sessionServiceOnly);
|
||||
}
|
||||
|
||||
@@ -337,19 +337,34 @@ public final class MediaSessionManager {
|
||||
|
||||
/**
|
||||
* Called when a {@link MediaSession2} is created.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
// TODO(jaewan): System API
|
||||
public SessionToken2 createSessionToken(@NonNull String callingPackage, @NonNull String id,
|
||||
@NonNull IMediaSession2 binder) {
|
||||
public boolean onSessionCreated(@NonNull SessionToken2 token) {
|
||||
if (token == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Bundle bundle = mService.createSessionToken(callingPackage, id, binder);
|
||||
return SessionToken2.fromBundle(bundle);
|
||||
return mService.onSessionCreated(token.toBundle());
|
||||
} catch (RemoteException e) {
|
||||
Log.wtf(TAG, "Cannot communicate with the service.", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Called when a {@link MediaSession2} is destroyed.
|
||||
* @hide
|
||||
*/
|
||||
// TODO(jaewan): System API
|
||||
public void onSessionDestroyed(@NonNull SessionToken2 token) {
|
||||
if (token == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mService.onSessionDestroyed(token.toBundle());
|
||||
} catch (RemoteException e) {
|
||||
Log.wtf(TAG, "Cannot communicate with the service.", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -367,7 +382,7 @@ public final class MediaSessionManager {
|
||||
try {
|
||||
List<Bundle> bundles = mService.getSessionTokens(
|
||||
/* activeSessionOnly */ true, /* sessionServiceOnly */ false);
|
||||
return toTokenList(bundles);
|
||||
return toTokenList(mContext, bundles);
|
||||
} catch (RemoteException e) {
|
||||
Log.wtf(TAG, "Cannot communicate with the service.", e);
|
||||
return Collections.emptyList();
|
||||
@@ -387,7 +402,7 @@ public final class MediaSessionManager {
|
||||
try {
|
||||
List<Bundle> bundles = mService.getSessionTokens(
|
||||
/* activeSessionOnly */ false, /* sessionServiceOnly */ true);
|
||||
return toTokenList(bundles);
|
||||
return toTokenList(mContext, bundles);
|
||||
} catch (RemoteException e) {
|
||||
Log.wtf(TAG, "Cannot communicate with the service.", e);
|
||||
return Collections.emptyList();
|
||||
@@ -410,18 +425,18 @@ public final class MediaSessionManager {
|
||||
try {
|
||||
List<Bundle> bundles = mService.getSessionTokens(
|
||||
/* activeSessionOnly */ false, /* sessionServiceOnly */ false);
|
||||
return toTokenList(bundles);
|
||||
return toTokenList(mContext, bundles);
|
||||
} catch (RemoteException e) {
|
||||
Log.wtf(TAG, "Cannot communicate with the service.", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private static List<SessionToken2> toTokenList(List<Bundle> bundles) {
|
||||
private static List<SessionToken2> toTokenList(Context context, List<Bundle> bundles) {
|
||||
List<SessionToken2> tokens = new ArrayList<>();
|
||||
if (bundles != null) {
|
||||
for (int i = 0; i < bundles.size(); i++) {
|
||||
SessionToken2 token = SessionToken2.fromBundle(bundles.get(i));
|
||||
SessionToken2 token = SessionToken2.fromBundle(context, bundles.get(i));
|
||||
if (token != null) {
|
||||
tokens.add(token);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ import java.util.List;
|
||||
* @hide
|
||||
*/
|
||||
public interface MediaController2Provider extends TransportControlProvider {
|
||||
void initialize();
|
||||
|
||||
void close_impl();
|
||||
SessionToken2 getSessionToken_impl();
|
||||
boolean isConnected_impl();
|
||||
|
||||
@@ -35,6 +35,8 @@ import java.util.List;
|
||||
* @hide
|
||||
*/
|
||||
public interface MediaSession2Provider extends TransportControlProvider {
|
||||
void initialize();
|
||||
|
||||
void close_impl();
|
||||
void setPlayer_impl(MediaPlayerInterface player);
|
||||
void setPlayer_impl(MediaPlayerInterface player, VolumeProvider volumeProvider);
|
||||
|
||||
34
media/java/android/media/update/SessionToken2Provider.java
Normal file
34
media/java/android/media/update/SessionToken2Provider.java
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2018 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.update;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public interface SessionToken2Provider {
|
||||
String getPackageName_impl();
|
||||
String getId_imp();
|
||||
int getType_impl();
|
||||
int getUid_impl();
|
||||
Bundle toBundle_impl();
|
||||
|
||||
int hashCode_impl();
|
||||
boolean equals_impl(Object obj);
|
||||
String toString_impl();
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import android.media.SessionToken2;
|
||||
import android.media.VolumeProvider;
|
||||
import android.media.update.MediaLibraryService2Provider.MediaLibrarySessionProvider;
|
||||
import android.media.update.MediaSession2Provider.ControllerInfoProvider;
|
||||
import android.os.Bundle;
|
||||
import android.os.IInterface;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.MediaControlView2;
|
||||
@@ -71,4 +72,8 @@ public interface StaticProvider {
|
||||
MediaLibrarySession instance, MediaPlayerInterface player, String id,
|
||||
VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity,
|
||||
Executor executor, MediaLibrarySessionCallback callback);
|
||||
SessionToken2Provider createSessionToken2(Context context, SessionToken2 instance,
|
||||
int uid, int type, String packageName, String serviceName, String id,
|
||||
IInterface sessionBinderInterface);
|
||||
SessionToken2 SessionToken2_fromBundle(Context context, Bundle bundle);
|
||||
}
|
||||
|
||||
@@ -44,138 +44,88 @@ class MediaSession2Record {
|
||||
private static final boolean DEBUG = true; // TODO(jaewan): Change
|
||||
|
||||
private final Context mContext;
|
||||
private final SessionToken2 mSessionToken;
|
||||
private final SessionDestroyedListener mSessionDestroyedListener;
|
||||
|
||||
// TODO(jaewan): Replace these with the mContext.getMainExecutor()
|
||||
private final Handler mMainHandler;
|
||||
private final Executor mMainExecutor;
|
||||
|
||||
private MediaController2 mController;
|
||||
private ControllerCallback mControllerCallback;
|
||||
|
||||
private int mSessionPid;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public MediaSession2Record(@NonNull Context context,
|
||||
public MediaSession2Record(@NonNull Context context, @NonNull SessionToken2 token,
|
||||
@NonNull SessionDestroyedListener listener) {
|
||||
mContext = context;
|
||||
mSessionToken = token;
|
||||
mSessionDestroyedListener = listener;
|
||||
|
||||
mMainHandler = new Handler(Looper.getMainLooper());
|
||||
mMainExecutor = (runnable) -> {
|
||||
mMainHandler.post(runnable);
|
||||
};
|
||||
}
|
||||
|
||||
public int getSessionPid() {
|
||||
return mSessionPid;
|
||||
mMainExecutor = (runnable) -> runnable.run();
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
public void onSessionDestroyed() {
|
||||
if (mController != null) {
|
||||
mControllerCallback.destroy();
|
||||
mController.close();
|
||||
// close() triggers ControllerCallback.onDisconnected() here already.
|
||||
mController = null;
|
||||
}
|
||||
mSessionPid = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create session token and tell server that session is now active.
|
||||
*
|
||||
* @param sessionPid session's pid
|
||||
* @return a token if successfully set, {@code null} if sanity check fails.
|
||||
*/
|
||||
// TODO(jaewan): also add uid for multiuser support
|
||||
@CallSuper
|
||||
public @Nullable
|
||||
SessionToken2 createSessionToken(int sessionPid, String packageName, String id,
|
||||
IMediaSession2 sessionBinder) {
|
||||
public boolean onSessionCreated(SessionToken2 token) {
|
||||
if (mController != null) {
|
||||
if (mSessionPid != sessionPid) {
|
||||
// A package uses the same id for session across the different process.
|
||||
return null;
|
||||
}
|
||||
// If a session becomes inactive and then active again very quickly, previous 'inactive'
|
||||
// may not have delivered yet. Check if it's the case and destroy controller before
|
||||
// creating its session record to prevents getXXTokens() API from returning duplicated
|
||||
// tokens.
|
||||
// TODO(jaewan): Change this. If developer is really creating two sessions with the same
|
||||
// id, this will silently invalidate previous session and no way for
|
||||
// developers to know that.
|
||||
// Instead, keep the list of static session ids from our APIs.
|
||||
// Also change Controller2Impl.onConnectionChanged / getController.
|
||||
// Also clean up ControllerCallback#destroy().
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Session is recreated almost immediately. " + this);
|
||||
}
|
||||
onSessionDestroyed();
|
||||
// Disclaimer: This may fail if following happens for an app.
|
||||
// Step 1) Create a session in the process #1
|
||||
// Step 2) Process #1 is killed
|
||||
// Step 3) Before the death of process #1 is delivered,
|
||||
// (i.e. ControllerCallback#onDisconnected is called),
|
||||
// new process is started and create another session with the same
|
||||
// id in the new process.
|
||||
// Step 4) fail!!! But this is tricky case that wouldn't happen in normal.
|
||||
Log.w(TAG, "Cannot create a new session with the id=" + token.getId() + " in the"
|
||||
+ " pkg=" + token.getPackageName() + ". ID should be unique in a package");
|
||||
return false;
|
||||
}
|
||||
mController = onCreateMediaController(packageName, id, sessionBinder);
|
||||
mSessionPid = sessionPid;
|
||||
return mController.getSessionToken();
|
||||
mController = new MediaController2(mContext, token, mMainExecutor,
|
||||
new ControllerCallback());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when session becomes active and needs controller to listen session's activeness.
|
||||
* <p>
|
||||
* Should be overridden by subclasses to create token with its own extra information.
|
||||
*/
|
||||
MediaController2 onCreateMediaController(
|
||||
String packageName, String id, IMediaSession2 sessionBinder) {
|
||||
SessionToken2 token = new SessionToken2(
|
||||
SessionToken2.TYPE_SESSION, packageName, id, null, sessionBinder);
|
||||
return createMediaController(token);
|
||||
}
|
||||
|
||||
final MediaController2 createMediaController(SessionToken2 token) {
|
||||
mControllerCallback = new ControllerCallback();
|
||||
return new MediaController2(mContext, token, mMainExecutor, mControllerCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return controller. Note that framework can only call oneway calls.
|
||||
* @return token
|
||||
*/
|
||||
public SessionToken2 getToken() {
|
||||
return mController == null ? null : mController.getSessionToken();
|
||||
return mSessionToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return controller
|
||||
*/
|
||||
public MediaController2 getController() {
|
||||
return mController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getToken() == null
|
||||
? "Token {null}"
|
||||
: "SessionRecord {pid=" + mSessionPid + ", " + getToken().toString() + "}";
|
||||
? "Token {null}" : "SessionRecord {" + getToken().toString() + "}";
|
||||
}
|
||||
|
||||
private class ControllerCallback extends MediaController2.ControllerCallback {
|
||||
private final AtomicBoolean mIsActive = new AtomicBoolean(true);
|
||||
|
||||
// This is called on the main thread with no lock. So place ensure followings.
|
||||
// This is called on the random thread with no lock. So place ensure followings.
|
||||
// 1. Don't touch anything in the parent class that needs synchronization.
|
||||
// All other APIs in the MediaSession2Record assumes that server would use them with
|
||||
// the lock hold.
|
||||
// 2. This can be called after the controller registered is released.
|
||||
// 2. This can be called after the controller registered is closed.
|
||||
@Override
|
||||
public void onDisconnected() {
|
||||
if (!mIsActive.get()) {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onDisconnected, token=" + getToken());
|
||||
}
|
||||
mSessionDestroyedListener.onSessionDestroyed(MediaSession2Record.this);
|
||||
}
|
||||
|
||||
// TODO(jaewan): Remove this API when we revisit createSessionToken()
|
||||
public void destroy() {
|
||||
mIsActive.set(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package com.android.server.media;
|
||||
|
||||
import static android.media.SessionToken2.TYPE_SESSION;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.INotificationManager;
|
||||
import android.app.KeyguardManager;
|
||||
@@ -28,6 +30,7 @@ import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.content.pm.UserInfo;
|
||||
@@ -130,15 +133,9 @@ public class MediaSessionService extends SystemService implements Monitor {
|
||||
private final List<MediaSession2Record> mSessions = new ArrayList<>();
|
||||
|
||||
private final MediaSession2Record.SessionDestroyedListener mSessionDestroyedListener =
|
||||
(MediaSession2Record record) -> {
|
||||
(record) -> {
|
||||
synchronized (mLock) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, record.toString() + " becomes inactive");
|
||||
}
|
||||
record.onSessionDestroyed();
|
||||
if (!(record instanceof MediaSessionService2Record)) {
|
||||
mSessions.remove(record);
|
||||
}
|
||||
destroySessionLocked(record);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -446,14 +443,16 @@ public class MediaSessionService extends SystemService implements Monitor {
|
||||
}
|
||||
|
||||
// TODO(jaewan): Query per users.
|
||||
// TODO(jaewan): Similar codes are also at the updatable. Can't we share codes?
|
||||
PackageManager manager = getContext().getPackageManager();
|
||||
List<ResolveInfo> services = new ArrayList<>();
|
||||
// If multiple actions are declared for a service, browser gets higher priority.
|
||||
List<ResolveInfo> libraryServices = getContext().getPackageManager().queryIntentServices(
|
||||
List<ResolveInfo> libraryServices = manager.queryIntentServices(
|
||||
new Intent(MediaLibraryService2.SERVICE_INTERFACE), PackageManager.GET_META_DATA);
|
||||
if (libraryServices != null) {
|
||||
services.addAll(libraryServices);
|
||||
}
|
||||
List<ResolveInfo> sessionServices = getContext().getPackageManager().queryIntentServices(
|
||||
List<ResolveInfo> sessionServices = manager.queryIntentServices(
|
||||
new Intent(MediaSessionService2.SERVICE_INTERFACE), PackageManager.GET_META_DATA);
|
||||
if (sessionServices != null) {
|
||||
services.addAll(sessionServices);
|
||||
@@ -468,12 +467,19 @@ public class MediaSessionService extends SystemService implements Monitor {
|
||||
continue;
|
||||
}
|
||||
ServiceInfo serviceInfo = services.get(i).serviceInfo;
|
||||
int uid;
|
||||
try {
|
||||
uid = manager.getPackageUid(serviceInfo.packageName,
|
||||
PackageManager.GET_META_DATA);
|
||||
} catch (NameNotFoundException e) {
|
||||
continue;
|
||||
}
|
||||
String id = (serviceInfo.metaData != null) ? serviceInfo.metaData.getString(
|
||||
MediaSessionService2.SERVICE_META_DATA) : null;
|
||||
// Do basic sanity check
|
||||
// TODO(jaewan): also santity check if it's protected with the system|privileged
|
||||
// permission
|
||||
boolean conflict = (getSessionRecordLocked(serviceInfo.name, id) != null);
|
||||
boolean conflict = (getSessionRecordLocked(uid, serviceInfo.name, id) != null);
|
||||
if (conflict) {
|
||||
Log.w(TAG, serviceInfo.packageName + " contains multiple"
|
||||
+ " MediaSessionService2s declared in the manifest with"
|
||||
@@ -481,10 +487,12 @@ public class MediaSessionService extends SystemService implements Monitor {
|
||||
+ serviceInfo.packageName + "/" + serviceInfo.name);
|
||||
} else {
|
||||
int type = (libraryServices.contains(services.get(i)))
|
||||
? SessionToken2.TYPE_LIBRARY_SERVICE : SessionToken2.TYPE_SESSION_SERVICE;
|
||||
MediaSessionService2Record record =
|
||||
new MediaSessionService2Record(getContext(), mSessionDestroyedListener,
|
||||
type, serviceInfo.packageName, serviceInfo.name, id);
|
||||
? SessionToken2.TYPE_LIBRARY_SERVICE
|
||||
: SessionToken2.TYPE_SESSION_SERVICE;
|
||||
SessionToken2 token = new SessionToken2(getContext(), uid, type,
|
||||
serviceInfo.packageName, serviceInfo.name, id, null);
|
||||
MediaSession2Record record = new MediaSession2Record(getContext(),
|
||||
token, mSessionDestroyedListener);
|
||||
mSessions.add(record);
|
||||
}
|
||||
}
|
||||
@@ -497,17 +505,27 @@ public class MediaSessionService extends SystemService implements Monitor {
|
||||
}
|
||||
}
|
||||
|
||||
MediaSession2Record getSessionRecordLocked(String packageName, String id) {
|
||||
private MediaSession2Record getSessionRecordLocked(int uid, String packageName, String id) {
|
||||
for (int i = 0; i < mSessions.size(); i++) {
|
||||
MediaSession2Record record = mSessions.get(i);
|
||||
if (record.getToken().getPackageName().equals(packageName)
|
||||
&& record.getToken().getId().equals(id)) {
|
||||
return record;
|
||||
SessionToken2 token = mSessions.get(i).getToken();
|
||||
if (token.getUid() == uid && token.getPackageName().equals(packageName)
|
||||
&& token.getId().equals(id)) {
|
||||
return mSessions.get(i);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void destroySessionLocked(MediaSession2Record record) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, record.toString() + " becomes inactive");
|
||||
}
|
||||
record.onSessionDestroyed();
|
||||
if (record.getToken().getType() == TYPE_SESSION) {
|
||||
mSessions.remove(record);
|
||||
}
|
||||
}
|
||||
|
||||
private void enforcePackageName(String packageName, int uid) {
|
||||
if (TextUtils.isEmpty(packageName)) {
|
||||
throw new IllegalArgumentException("packageName may not be empty");
|
||||
@@ -1410,29 +1428,55 @@ public class MediaSessionService extends SystemService implements Monitor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle createSessionToken(String sessionPackage, String id,
|
||||
IMediaSession2 sessionBinder) throws RemoteException {
|
||||
int uid = Binder.getCallingUid();
|
||||
int pid = Binder.getCallingPid();
|
||||
|
||||
MediaSession2Record record;
|
||||
SessionToken2 token;
|
||||
// TODO(jaewan): Add sanity check for the token if calling package is from uid.
|
||||
public boolean onSessionCreated(Bundle sessionToken) {
|
||||
final int uid = Binder.getCallingUid();
|
||||
final int pid = Binder.getCallingPid();
|
||||
final SessionToken2 token = SessionToken2.fromBundle(getContext(), sessionToken);
|
||||
if (token == null || token.getUid() != uid) {
|
||||
Log.w(TAG, "onSessionCreated failed, expected caller uid=" + token.getUid()
|
||||
+ " but from uid=" + uid);
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onSessionCreated " + token);
|
||||
}
|
||||
synchronized (mLock) {
|
||||
record = getSessionRecordLocked(sessionPackage, id);
|
||||
if (record == null) {
|
||||
record = new MediaSession2Record(getContext(), mSessionDestroyedListener);
|
||||
mSessions.add(record);
|
||||
}
|
||||
token = record.createSessionToken(pid, sessionPackage, id, sessionBinder);
|
||||
if (token == null) {
|
||||
Log.d(TAG, "failed to create session token for " + sessionPackage
|
||||
+ " from pid=" + pid + ". Previously " + record);
|
||||
MediaSession2Record record = getSessionRecordLocked(
|
||||
uid, token.getPackageName(), token.getId());
|
||||
if (record != null) {
|
||||
return record.onSessionCreated(token);
|
||||
} else {
|
||||
Log.d(TAG, "session " + token + " is created");
|
||||
record = new MediaSession2Record(
|
||||
getContext(), token, mSessionDestroyedListener);
|
||||
mSessions.add(record);
|
||||
return record.onSessionCreated(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionDestroyed(Bundle sessionToken) {
|
||||
final int uid = Binder.getCallingUid();
|
||||
final int pid = Binder.getCallingPid();
|
||||
final SessionToken2 token = SessionToken2.fromBundle(getContext(), sessionToken);
|
||||
if (token == null || token.getUid() != uid) {
|
||||
Log.w(TAG, "onSessionDestroyed failed, expected caller uid=" + token.getUid()
|
||||
+ " but from uid=" + uid);
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onSessionDestroyed " + token);
|
||||
}
|
||||
synchronized (mLock) {
|
||||
MediaSession2Record record = getSessionRecordLocked(
|
||||
uid, token.getPackageName(), token.getId());
|
||||
if (record != null) {
|
||||
record.onSessionDestroyed();
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Cannot find a session record to destroy. uid=" + uid
|
||||
+ ", pkg=" + token.getPackageName() + ", id=" + token.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
return token == null ? null : token.toBundle();
|
||||
}
|
||||
|
||||
// TODO(jaewan): Protect this API with permission
|
||||
@@ -1444,8 +1488,8 @@ public class MediaSessionService extends SystemService implements Monitor {
|
||||
synchronized (mLock) {
|
||||
for (int i = 0; i < mSessions.size(); i++) {
|
||||
MediaSession2Record record = mSessions.get(i);
|
||||
boolean isSessionService = (record instanceof MediaSessionService2Record);
|
||||
boolean isActive = record.getSessionPid() != 0;
|
||||
boolean isSessionService = (record.getToken().getType() != TYPE_SESSION);
|
||||
boolean isActive = record.getController() != null;
|
||||
if ((!activeSessionOnly && isSessionService)
|
||||
|| (!sessionServiceOnly && isActive)) {
|
||||
SessionToken2 token = record.getToken();
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 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.content.Context;
|
||||
import android.media.IMediaSession2;
|
||||
import android.media.MediaController2;
|
||||
import android.media.SessionToken2;
|
||||
import android.media.MediaSessionService2;
|
||||
|
||||
/**
|
||||
* Records a {@link MediaSessionService2}.
|
||||
* <p>
|
||||
* Owner of this object should handle synchronization.
|
||||
*/
|
||||
class MediaSessionService2Record extends MediaSession2Record {
|
||||
private static final boolean DEBUG = true; // TODO(jaewan): Modify
|
||||
private static final String TAG = "SessionService2Record";
|
||||
|
||||
private final int mType;
|
||||
private final String mServiceName;
|
||||
private final SessionToken2 mToken;
|
||||
|
||||
public MediaSessionService2Record(Context context,
|
||||
SessionDestroyedListener sessionDestroyedListener, int type,
|
||||
String packageName, String serviceName, String id) {
|
||||
super(context, sessionDestroyedListener);
|
||||
mType = type;
|
||||
mServiceName = serviceName;
|
||||
mToken = new SessionToken2(mType, packageName, id, mServiceName, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overriden to change behavior of
|
||||
* {@link #createSessionToken(int, String, String, IMediaSession2)}}.
|
||||
*/
|
||||
@Override
|
||||
MediaController2 onCreateMediaController(
|
||||
String packageName, String id, IMediaSession2 sessionBinder) {
|
||||
SessionToken2 token = new SessionToken2(mType, packageName, id, mServiceName, sessionBinder);
|
||||
return createMediaController(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return token with no session binder information.
|
||||
*/
|
||||
@Override
|
||||
public SessionToken2 getToken() {
|
||||
return mToken;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user