[go: nahoru, domu]

blob: b6c7f2688630f3c2124b5f3ead838940d6c36deb [file] [log] [blame]
/*
* 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 androidx.media2.session;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
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.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.text.TextUtils;
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 androidx.media2.common.ClassVerificationHelper;
import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelable;
import androidx.versionedparcelable.VersionedParcelize;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
* Represents an ongoing {@link MediaSession} or a {@link MediaSessionService}. 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
* MediaController} to communicate with the session.
*
* <p>It can be also obtained by {@link MediaSessionManager}.
*
* @deprecated androidx.media2 is deprecated. Please migrate to <a
* href="https://developer.android.com/guide/topics/media/media3">androidx.media3</a>.
*/
// 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.
// This helps controller apps to keep target of dispatching media key events in uniform way.
// For details about the reason, see following. (Android O+)
// android.media.session.MediaSessionManager.Callback#onAddressedPlayerChanged
@Deprecated
@VersionedParcelize
public final class SessionToken implements VersionedParcelable {
private static final String TAG = "SessionToken";
private static final long WAIT_TIME_MS_FOR_SESSION_READY = 300;
private static final int MSG_SEND_TOKEN2_FOR_LEGACY_SESSION = 1000;
/**
*/
@RestrictTo(LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE, TYPE_LIBRARY_SERVICE})
public @interface TokenType {
}
/**
* Type for {@link MediaSession}.
*/
public static final int TYPE_SESSION = 0;
/**
* Type for {@link MediaSessionService}.
*/
public static final int TYPE_SESSION_SERVICE = 1;
/**
* Type for {@link MediaLibraryService}.
*/
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;
@ParcelField(1)
SessionTokenImpl mImpl;
// WARNING: Adding a new ParcelField may break old library users (b/152830728)
/**
* Constructor for the token. You can create token of {@link MediaSessionService},
* {@link MediaLibraryService} or {@link MediaBrowserServiceCompat} for
* {@link MediaController} or {@link MediaBrowser}.
*
* @param context The context.
* @param serviceComponent The component name of the service.
*/
public SessionToken(@NonNull Context context, @NonNull ComponentName serviceComponent) {
if (context == null) {
throw new NullPointerException("context shouldn't be null");
}
if (serviceComponent == null) {
throw new NullPointerException("serviceComponent shouldn't be null");
}
final PackageManager manager = context.getPackageManager();
final int uid = getUid(manager, serviceComponent.getPackageName());
final int type;
if (isInterfaceDeclared(manager, MediaLibraryService.SERVICE_INTERFACE,
serviceComponent)) {
type = TYPE_LIBRARY_SERVICE;
} else if (isInterfaceDeclared(manager, MediaSessionService.SERVICE_INTERFACE,
serviceComponent)) {
type = TYPE_SESSION_SERVICE;
} else if (isInterfaceDeclared(manager,
MediaBrowserServiceCompat.SERVICE_INTERFACE, serviceComponent)) {
type = TYPE_BROWSER_SERVICE_LEGACY;
} else {
throw new IllegalArgumentException(serviceComponent + " doesn't implement none of"
+ " MediaSessionService, MediaLibraryService, MediaBrowserService nor"
+ " MediaBrowserServiceCompat. Use service's full name");
}
if (type != TYPE_BROWSER_SERVICE_LEGACY) {
mImpl = new SessionTokenImplBase(serviceComponent, uid, type);
} else {
mImpl = new SessionTokenImplLegacy(serviceComponent, uid);
}
}
SessionToken(SessionTokenImpl impl) {
mImpl = impl;
}
/**
* Used for {@link VersionedParcelable}
*/
SessionToken() {
// do nothing
}
@Override
public int hashCode() {
return mImpl.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof SessionToken)) {
return false;
}
SessionToken other = (SessionToken) 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 of the session
*/
@NonNull
public String getPackageName() {
return mImpl.getPackageName();
}
/**
* @return service name of the session. Can be {@code null} for {@link #TYPE_SESSION}.
*/
@Nullable
public String getServiceName() {
return mImpl.getServiceName();
}
/**
* @return component name of the session. Can be {@code null} for {@link #TYPE_SESSION}.
*/
@RestrictTo(LIBRARY)
public ComponentName getComponentName() {
return mImpl.getComponentName();
}
/**
* @return type of the token
* @see #TYPE_SESSION
* @see #TYPE_SESSION_SERVICE
* @see #TYPE_LIBRARY_SERVICE
*/
@TokenType
public int getType() {
return mImpl.getType();
}
/**
* @return extras of the token
* @see MediaSession.Builder#setExtras(Bundle)
*/
@NonNull
public Bundle getExtras() {
Bundle extras = mImpl.getExtras();
if (extras == null || MediaUtils.doesBundleHaveCustomParcelable(extras)) {
return Bundle.EMPTY;
}
return new Bundle(extras);
}
/**
*/
@RestrictTo(LIBRARY)
public boolean isLegacySession() {
return mImpl.isLegacySession();
}
/**
*/
@RestrictTo(LIBRARY)
public Object getBinder() {
return mImpl.getBinder();
}
/**
* Creates SessionToken object from MediaSessionCompat.Token.
* When the SessionToken is ready, OnSessionTokenCreateListener will be called.
*/
@RestrictTo(LIBRARY)
public static void createSessionToken(@NonNull final Context context,
@NonNull final MediaSessionCompat.Token compatToken,
@NonNull final OnSessionTokenCreatedListener listener) {
if (context == null) {
throw new NullPointerException("context shouldn't be null");
}
if (compatToken == null) {
throw new NullPointerException("compatToken shouldn't be null");
}
if (listener == null) {
throw new NullPointerException("listener shouldn't be null");
}
// If the compat token already has the SessionToken in itself, just notify with that token.
VersionedParcelable token2 = compatToken.getSession2Token();
if (token2 instanceof SessionToken) {
listener.onSessionTokenCreated(compatToken, (SessionToken) token2);
return;
}
// Try retrieving media2 token by connecting to the session.
final MediaControllerCompat controller = createMediaControllerCompat(context, compatToken);
final String packageName = controller.getPackageName();
final int uid = getUid(context.getPackageManager(), packageName);
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) {
return;
}
controller.unregisterCallback((MediaControllerCompat.Callback) msg.obj);
// MediaControllerCompat.Callback#onSessionReady() is not called, which means
// that the connected session is a framework MediaSession instance.
SessionToken resultToken = new SessionToken(new SessionTokenImplLegacy(
compatToken, packageName, uid, controller.getSessionInfo()));
// To prevent repeating this process with the same compat token, put the result
// media2 token inside of the compat token.
compatToken.setSession2Token(resultToken);
listener.onSessionTokenCreated(compatToken, resultToken);
quitHandlerThread(thread);
}
}
};
MediaControllerCompat.Callback callback = new MediaControllerCompat.Callback() {
@Override
public void onSessionReady() {
// The connected session is a MediaSessionCompat instance.
synchronized (listener) {
handler.removeMessages(MSG_SEND_TOKEN2_FOR_LEGACY_SESSION);
controller.unregisterCallback(this);
// TODO: Add logic for getting media2 token in API 21- by using binder.
SessionToken resultToken;
if (compatToken.getSession2Token() instanceof SessionToken) {
// TODO(b/132928776): Add tests for this code path.
// The connected MediaSessionCompat is created by media2.MediaSession
resultToken = (SessionToken) compatToken.getSession2Token();
} else {
// The connected MediaSessionCompat is standalone.
resultToken = new SessionToken(new SessionTokenImplLegacy(
compatToken, packageName, uid, controller.getSessionInfo()));
// To prevent repeating this process with the same compat token,
// put the result media2 token inside of the compat token.
compatToken.setSession2Token(resultToken);
}
listener.onSessionTokenCreated(compatToken, resultToken);
quitHandlerThread(thread);
}
}
};
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);
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
static void quitHandlerThread(HandlerThread thread) {
if (Build.VERSION.SDK_INT >= 18) {
ClassVerificationHelper.HandlerThread.Api18.quitSafely(thread);
} else {
thread.quit();
}
}
@SuppressWarnings("deprecation")
private static boolean isInterfaceDeclared(PackageManager manager, String serviceInterface,
ComponentName serviceComponent) {
Intent serviceIntent = new Intent(serviceInterface);
// Use queryIntentServices to find services with MediaLibraryService.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 true;
}
}
}
return false;
}
@SuppressWarnings("deprecation")
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);
}
}
private static MediaControllerCompat createMediaControllerCompat(Context context,
MediaSessionCompat.Token sessionToken) {
return new MediaControllerCompat(context, sessionToken);
}
/**
* Interface definition of a listener to be invoked when a {@link SessionToken token2} object
* is created from a {@link MediaSessionCompat.Token compat token}.
*
* @see #createSessionToken
*/
@RestrictTo(LIBRARY)
public interface OnSessionTokenCreatedListener {
/**
* Called when SessionToken object is created.
*
* @param compatToken the compat token used for creating {@code token2}
* @param sessionToken the created SessionToken object
*/
void onSessionTokenCreated(MediaSessionCompat.Token compatToken, SessionToken sessionToken);
}
interface SessionTokenImpl extends VersionedParcelable {
boolean isLegacySession();
int getUid();
@NonNull String getPackageName();
@Nullable String getServiceName();
@Nullable ComponentName getComponentName();
@TokenType int getType();
@Nullable Bundle getExtras();
Object getBinder();
}
}