/*
* 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 androidx.media2;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.RemoteException;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.media.MediaBrowserServiceCompat;
import androidx.media.MediaSessionManager;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.concurrent.Executor;
/**
* Represents an ongoing {@link MediaSession2} or a {@link MediaSessionService2}.
* If it's representing a session service, it may not be ongoing.
* <p>
* This may be passed to apps by the session owner to allow them to create a
* {@link MediaController2} to communicate with the session.
* <p>
* It can be also obtained by {@link MediaSessionManager}.
*/
// New version of MediaSession.Token for following reasons
// - Stop implementing Parcelable for updatable support
// - Represent session and library service (formerly browser service) in one class.
// Previously MediaSession.Token was for session and ComponentName was for service.
public final class SessionToken2 {
private static final String TAG = "SessionToken2";
private static final long WAIT_TIME_MS_FOR_SESSION_READY = 300;
private static final int MSG_SEND_TOKEN2_FOR_LEGACY_SESSION = 1000;
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE, TYPE_LIBRARY_SERVICE})
public @interface TokenType {
}
/**
* Type for {@link MediaSession2}.
*/
public static final int TYPE_SESSION = 0;
/**
* Type for {@link MediaSessionService2}.
*/
public static final int TYPE_SESSION_SERVICE = 1;
/**
* Type for {@link MediaLibraryService2}.
*/
public static final int TYPE_LIBRARY_SERVICE = 2;
/**
* Type for {@link MediaSessionCompat}.
*/
static final int TYPE_SESSION_LEGACY = 100;
/**
* Type for {@link MediaBrowserServiceCompat}.
*/
static final int TYPE_BROWSER_SERVICE_LEGACY = 101;
// From the return value of android.os.Process.getUidForName(String) when error
static final int UID_UNKNOWN = -1;
static final String KEY_UID = "android.media.token.uid";
static final String KEY_TYPE = "android.media.token.type";
static final String KEY_PACKAGE_NAME = "android.media.token.package_name";
static final String KEY_SERVICE_NAME = "android.media.token.service_name";
static final String KEY_SESSION_ID = "android.media.token.session_id";
static final String KEY_SESSION_BINDER = "android.media.token.session_binder";
static final String KEY_TOKEN_LEGACY = "android.media.token.LEGACY";
private final SessionToken2Impl mImpl;
/**
* Constructor for the token. You can create token of {@link MediaSessionService2},
* {@link MediaLibraryService2} nor {@link MediaBrowserServiceCompat} for
* {@link MediaController2} or {@link MediaBrowser2}.
*
* @param context The context.
* @param serviceComponent The component name of the media browser service.
*/
public SessionToken2(@NonNull Context context, @NonNull ComponentName serviceComponent) {
final PackageManager manager = context.getPackageManager();
final int uid = getUid(manager, serviceComponent.getPackageName());
final String sessionId;
final int type;
String id = getSessionIdFromService(manager, MediaLibraryService2.SERVICE_INTERFACE,
serviceComponent);
if (id != null) {
sessionId = id;
type = TYPE_LIBRARY_SERVICE;
} else {
// retry with session service
id = getSessionIdFromService(manager, MediaSessionService2.SERVICE_INTERFACE,
serviceComponent);
if (id != null) {
sessionId = id;
type = TYPE_SESSION_SERVICE;
} else {
// Last retry with media browser service (compat)
sessionId = getSessionIdFromService(manager,
MediaBrowserServiceCompat.SERVICE_INTERFACE, serviceComponent);
type = TYPE_BROWSER_SERVICE_LEGACY;
}
}
if (sessionId == null) {
throw new IllegalArgumentException(serviceComponent + " doesn't implement none of"
+ " MediaSessionService2, MediaLibraryService2, MediaBrowserService nor"
+ " MediaBrowserServiceCompat. Use service's full name.");
}
if (type != TYPE_BROWSER_SERVICE_LEGACY) {
mImpl = new SessionToken2ImplBase(serviceComponent, uid, sessionId, type);
} else {
mImpl = new SessionToken2ImplLegacy(serviceComponent, uid, sessionId);
}
}
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
SessionToken2(SessionToken2Impl impl) {
mImpl = impl;
}
@Override
public int hashCode() {
return mImpl.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof SessionToken2)) {
return false;
}
SessionToken2 other = (SessionToken2) obj;
return mImpl.equals(other.mImpl);
}
@Override
public String toString() {
return mImpl.toString();
}
/**
* @return uid of the session
*/
public int getUid() {
return mImpl.getUid();
}
/**
* @return package name
*/
public @NonNull String getPackageName() {
return mImpl.getPackageName();
}
/**
* @return service name. Can be {@code null} for TYPE_SESSION.
*/
public @Nullable String getServiceName() {
return mImpl.getServiceName();
}
/**
* @hide
* @return component name of this session token. Can be null for TYPE_SESSION.
*/
@RestrictTo(LIBRARY_GROUP)
public ComponentName getComponentName() {
return mImpl.getComponentName();
}
/**
* @return id
*/
public String getId() {
return mImpl.getSessionId();
}
/**
* @return type of the token
* @see #TYPE_SESSION
* @see #TYPE_SESSION_SERVICE
* @see #TYPE_LIBRARY_SERVICE
*/
public @TokenType int getType() {
return mImpl.getType();
}
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public boolean isLegacySession() {
return mImpl.isLegacySession();
}
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public Object getBinder() {
return mImpl.getBinder();
}
/**
* Create a {@link Bundle} from this token to share it across processes.
* @return Bundle
*/
public Bundle toBundle() {
return mImpl.toBundle();
}
/**
* Create a token from the bundle, exported by {@link #toBundle()}.
*
* @param bundle
* @return SessionToken2 object
*/
public static SessionToken2 fromBundle(@NonNull Bundle bundle) {
if (bundle == null) {
return null;
}
final int type = bundle.getInt(KEY_TYPE, -1);
if (type == TYPE_SESSION_LEGACY) {
return new SessionToken2(SessionToken2ImplLegacy.fromBundle(bundle));
} else {
return new SessionToken2(SessionToken2ImplBase.fromBundle(bundle));
}
}
/**
* Creates SessionToken2 object from MediaSessionCompat.Token.
* When the SessionToken2 is ready, OnSessionToken2CreateListner will be called.
*
* TODO: Consider to use this in the constructor of MediaController2.
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public static void createSessionToken2(@NonNull final Context context,
@NonNull final MediaSessionCompat.Token token, @NonNull final Executor executor,
@NonNull final OnSessionToken2CreatedListener listener) {
if (context == null) {
throw new IllegalArgumentException("context shouldn't be null");
}
if (token == null) {
throw new IllegalArgumentException("token shouldn't be null");
}
if (executor == null) {
throw new IllegalArgumentException("executor shouldn't be null");
}
if (listener == null) {
throw new IllegalArgumentException("listener shouldn't be null");
}
try {
Bundle token2Bundle = token.getSessionToken2Bundle();
if (token2Bundle != null) {
notifySessionToken2Created(executor, listener, token,
SessionToken2.fromBundle(token2Bundle));
return;
}
final MediaControllerCompat controller = new MediaControllerCompat(context, token);
final int uid = getUid(context.getPackageManager(), controller.getPackageName());
final SessionToken2 token2ForLegacySession = new SessionToken2(
new SessionToken2ImplLegacy(token, controller.getPackageName(), uid));
final HandlerThread thread = new HandlerThread(TAG);
thread.start();
final Handler handler = new Handler(thread.getLooper()) {
@Override
public void handleMessage(Message msg) {
synchronized (listener) {
if (msg.what == MSG_SEND_TOKEN2_FOR_LEGACY_SESSION) {
// token for framework session.
controller.unregisterCallback((MediaControllerCompat.Callback) msg.obj);
token.setSessionToken2Bundle(token2ForLegacySession.toBundle());
notifySessionToken2Created(executor, listener, token,
token2ForLegacySession);
if (Build.VERSION.SDK_INT >= 18) {
thread.quitSafely();
} else {
thread.quit();
}
}
}
}
};
MediaControllerCompat.Callback callback = new MediaControllerCompat.Callback() {
@Override
public void onSessionReady() {
synchronized (listener) {
handler.removeMessages(MSG_SEND_TOKEN2_FOR_LEGACY_SESSION);
controller.unregisterCallback(this);
if (token.getSessionToken2Bundle() == null) {
token.setSessionToken2Bundle(token2ForLegacySession.toBundle());
}
notifySessionToken2Created(executor, listener, token,
SessionToken2.fromBundle(token.getSessionToken2Bundle()));
if (Build.VERSION.SDK_INT >= 18) {
thread.quitSafely();
} else {
thread.quit();
}
}
}
};
synchronized (listener) {
controller.registerCallback(callback, handler);
Message msg = handler.obtainMessage(MSG_SEND_TOKEN2_FOR_LEGACY_SESSION, callback);
handler.sendMessageDelayed(msg, WAIT_TIME_MS_FOR_SESSION_READY);
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to create session token2.", e);
}
}
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public static String getSessionId(ResolveInfo resolveInfo) {
if (resolveInfo == null || resolveInfo.serviceInfo == null) {
return null;
} else if (resolveInfo.serviceInfo.metaData == null) {
return "";
} else {
return resolveInfo.serviceInfo.metaData.getString(
MediaSessionService2.SERVICE_META_DATA_SESSION_ID, "");
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
static void notifySessionToken2Created(final Executor executor,
final OnSessionToken2CreatedListener listener, final MediaSessionCompat.Token token,
final SessionToken2 token2) {
executor.execute(new Runnable() {
@Override
public void run() {
listener.onSessionToken2Created(token, token2);
}
});
}
private static String getSessionIdFromService(PackageManager manager, String serviceInterface,
ComponentName serviceComponent) {
Intent serviceIntent = new Intent(serviceInterface);
// Use queryIntentServices to find services with MediaLibraryService2.SERVICE_INTERFACE.
// We cannot use resolveService with intent specified class name, because resolveService
// ignores actions if Intent.setClassName() is specified.
serviceIntent.setPackage(serviceComponent.getPackageName());
List<ResolveInfo> list = manager.queryIntentServices(
serviceIntent, PackageManager.GET_META_DATA);
if (list != null) {
for (int i = 0; i < list.size(); i++) {
ResolveInfo resolveInfo = list.get(i);
if (resolveInfo == null || resolveInfo.serviceInfo == null) {
continue;
}
if (TextUtils.equals(
resolveInfo.serviceInfo.name, serviceComponent.getClassName())) {
return SessionToken2.getSessionId(resolveInfo);
}
}
}
return null;
}
private static int getUid(PackageManager manager, String packageName) {
try {
return manager.getApplicationInfo(packageName, 0).uid;
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalArgumentException("Cannot find package " + packageName);
}
}
/**
* @hide
* Interface definition of a listener to be invoked when a {@link SessionToken2 token2} object
* is created from a {@link MediaSessionCompat.Token compat token}.
*
* @see #createSessionToken2
*/
@RestrictTo(LIBRARY_GROUP)
public interface OnSessionToken2CreatedListener {
/**
* Called when SessionToken2 object is created.
*
* @param token the compat token used for creating {@code token2}
* @param token2 the created SessionToken2 object
*/
void onSessionToken2Created(MediaSessionCompat.Token token, SessionToken2 token2);
}
interface SessionToken2Impl {
boolean isLegacySession();
int getUid();
@NonNull String getPackageName();
@Nullable String getServiceName();
@Nullable ComponentName getComponentName();
String getSessionId();
@TokenType int getType();
Bundle toBundle();
Object getBinder();
}
}