public final class

SessionToken

extends java.lang.Object

implements Bundleable

 java.lang.Object

↳androidx.media3.session.SessionToken

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-session', version: '1.0.0-alpha03'

  • groupId: androidx.media3
  • artifactId: media3-session
  • version: 1.0.0-alpha03

Artifact androidx.media3:media3-session:1.0.0-alpha03 it located at Google repository (https://maven.google.com/)

Overview

A token that represents an ongoing MediaSession or a service (MediaSessionService, MediaLibraryService, or MediaBrowserServiceCompat). If it represents a service, it may not be ongoing.

This may be passed to apps by the session owner to allow them to create a MediaController or a MediaBrowser to communicate with the session.

It can also be obtained by SessionToken.getAllServiceTokens(Context).

Summary

Fields
public static final Bundleable.Creator<SessionToken>CREATOR

Object that can restore SessionToken from a .

public static final intTYPE_LIBRARY_SERVICE

Type for MediaLibraryService.

public static final intTYPE_SESSION

Type for MediaSession.

public static final intTYPE_SESSION_SERVICE

Type for MediaSessionService.

Constructors
publicSessionToken(Context context, ComponentName serviceComponent)

Creates a token for MediaController or MediaBrowser to connect to one of MediaSessionService, MediaLibraryService, or MediaBrowserServiceCompat.

Methods
public static <any>createSessionToken(Context context, java.lang.Object compatToken)

Creates a token from .

public booleanequals(java.lang.Object obj)

public static <any>getAllServiceTokens(Context context)

Returns a of SessionToken for media session services; MediaSessionService, MediaLibraryService, and MediaBrowserServiceCompat regardless of their activeness.

public BundlegetExtras()

Returns the extra of this token.

public java.lang.StringgetPackageName()

Returns the package name of the session

public java.lang.StringgetServiceName()

Returns the service name of the session.

public intgetSessionVersion()

Returns the library version of the session if the type is SessionToken.TYPE_SESSION.

public intgetType()

Returns the type of this token.

public intgetUid()

Returns the uid of the session

public inthashCode()

public BundletoBundle()

public java.lang.StringtoString()

from java.lang.Objectclone, finalize, getClass, notify, notifyAll, wait, wait, wait

Fields

public static final int TYPE_SESSION

Type for MediaSession.

public static final int TYPE_SESSION_SERVICE

Type for MediaSessionService.

public static final int TYPE_LIBRARY_SERVICE

Type for MediaLibraryService.

public static final Bundleable.Creator<SessionToken> CREATOR

Object that can restore SessionToken from a .

Constructors

public SessionToken(Context context, ComponentName serviceComponent)

Creates a token for MediaController or MediaBrowser to connect to one of MediaSessionService, MediaLibraryService, or MediaBrowserServiceCompat.

Parameters:

context: The context.
serviceComponent: The component name of the service.

Methods

public int hashCode()

public boolean equals(java.lang.Object obj)

public java.lang.String toString()

public int getUid()

Returns the uid of the session

public java.lang.String getPackageName()

Returns the package name of the session

public java.lang.String getServiceName()

Returns the service name of the session. It will be an empty string if the type is SessionToken.TYPE_SESSION.

public int getType()

Returns the type of this token. One of SessionToken.TYPE_SESSION, SessionToken.TYPE_SESSION_SERVICE, or SessionToken.TYPE_LIBRARY_SERVICE.

public int getSessionVersion()

Returns the library version of the session if the type is SessionToken.TYPE_SESSION. Otherwise, it returns 0.

It will be the same as MediaLibraryInfo.VERSION_INT of the session, or less than 1000000 if the session is a legacy session.

public Bundle getExtras()

Returns the extra of this token.

See also: MediaSession.Builder.setExtras(Bundle)

public static <any> createSessionToken(Context context, java.lang.Object compatToken)

Creates a token from .

Returns:

a of SessionToken

public static <any> getAllServiceTokens(Context context)

Returns a of SessionToken for media session services; MediaSessionService, MediaLibraryService, and MediaBrowserServiceCompat regardless of their activeness. This set represents media apps that publish MediaSession.

The app targeting API level 30 or higher must include a element in their manifest to get service tokens of other apps. See the following example and this guide for more information.

 
   
 
 
   
 
 
   
 
 

public Bundle toBundle()

Source

/*
 * 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.media3.session;

import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static java.lang.annotation.ElementType.TYPE_USE;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.ResultReceiver;
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.Nullable;
import androidx.media.MediaBrowserServiceCompat;
import androidx.media3.common.Bundleable;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;

/**
 * A token that represents an ongoing {@link MediaSession} or a service ({@link
 * MediaSessionService}, {@link MediaLibraryService}, or {@link MediaBrowserServiceCompat}). If it
 * represents a 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} or a {@link MediaBrowser} to communicate with the session.
 *
 * <p>It can also be obtained by {@link #getAllServiceTokens(Context)}.
 */
// 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
public final class SessionToken implements Bundleable {

  private static final long WAIT_TIME_MS_FOR_SESSION3_TOKEN = 500;

  /** Types of {@link SessionToken}. */
  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @Target(TYPE_USE)
  @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}. */
  /* package */ static final int TYPE_SESSION_LEGACY = 100;

  /** Type for {@link MediaBrowserServiceCompat}. */
  /* package */ static final int TYPE_BROWSER_SERVICE_LEGACY = 101;

  private final SessionTokenImpl impl;

  /**
   * Creates a token for {@link MediaController} or {@link MediaBrowser} to connect to one of {@link
   * MediaSessionService}, {@link MediaLibraryService}, or {@link MediaBrowserServiceCompat}.
   *
   * @param context The context.
   * @param serviceComponent The component name of the service.
   */
  public SessionToken(Context context, ComponentName serviceComponent) {
    checkNotNull(context, "context must not be null");
    checkNotNull(serviceComponent, "serviceComponent must not be null");
    PackageManager manager = context.getPackageManager();
    int uid = getUid(manager, serviceComponent.getPackageName());

    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) {
      impl = new SessionTokenImplBase(serviceComponent, uid, type);
    } else {
      impl = new SessionTokenImplLegacy(serviceComponent, uid);
    }
  }

  /* package */ SessionToken(
      int uid,
      int type,
      int version,
      String packageName,
      IMediaSession iSession,
      Bundle tokenExtras) {
    impl = new SessionTokenImplBase(uid, type, version, packageName, iSession, tokenExtras);
  }

  /* package */ SessionToken(Context context, MediaSessionCompat.Token compatToken) {
    checkNotNull(context, "context must not be null");
    checkNotNull(compatToken, "compatToken must not be null");

    MediaControllerCompat controller = createMediaControllerCompat(context, compatToken);

    String packageName = controller.getPackageName();
    int uid = getUid(context.getPackageManager(), packageName);
    Bundle extras = controller.getSessionInfo();

    impl = new SessionTokenImplLegacy(compatToken, packageName, uid, extras);
  }

  /* package */ SessionToken(SessionTokenImpl impl) {
    this.impl = impl;
  }

  private SessionToken(Bundle bundle) {
    checkArgument(bundle.containsKey(keyForField(FIELD_IMPL_TYPE)), "Impl type needs to be set.");
    @SessionTokenImplType int implType = bundle.getInt(keyForField(FIELD_IMPL_TYPE));
    Bundle implBundle = checkNotNull(bundle.getBundle(keyForField(FIELD_IMPL)));
    if (implType == IMPL_TYPE_BASE) {
      impl = SessionTokenImplBase.CREATOR.fromBundle(implBundle);
    } else {
      impl = SessionTokenImplLegacy.CREATOR.fromBundle(implBundle);
    }
  }

  @Override
  public int hashCode() {
    return impl.hashCode();
  }

  @Override
  public boolean equals(@Nullable Object obj) {
    if (!(obj instanceof SessionToken)) {
      return false;
    }
    SessionToken other = (SessionToken) obj;
    return impl.equals(other.impl);
  }

  @Override
  public String toString() {
    return impl.toString();
  }

  /** Returns the uid of the session */
  public int getUid() {
    return impl.getUid();
  }

  /** Returns the package name of the session */
  public String getPackageName() {
    return impl.getPackageName();
  }

  /**
   * Returns the service name of the session. It will be an empty string if the {@link #getType()
   * type} is {@link #TYPE_SESSION}.
   */
  public String getServiceName() {
    return impl.getServiceName();
  }

  /**
   * Returns the component name of the session. It will be {@code null} if the {@link #getType()
   * type} is {@link #TYPE_SESSION}.
   */
  @Nullable
  /* package */ ComponentName getComponentName() {
    return impl.getComponentName();
  }

  /**
   * Returns the type of this token. One of {@link #TYPE_SESSION}, {@link #TYPE_SESSION_SERVICE}, or
   * {@link #TYPE_LIBRARY_SERVICE}.
   */
  public @TokenType int getType() {
    return impl.getType();
  }

  /**
   * Returns the library version of the session if the {@link #getType() type} is {@link
   * #TYPE_SESSION}. Otherwise, it returns {@code 0}.
   *
   * <p>It will be the same as {@link MediaLibraryInfo#VERSION_INT} of the session, or less than
   * {@code 1000000} if the session is a legacy session.
   */
  public int getSessionVersion() {
    return impl.getSessionVersion();
  }

  /**
   * Returns the extra {@link Bundle} of this token.
   *
   * @see MediaSession.Builder#setExtras(Bundle)
   */
  public Bundle getExtras() {
    return impl.getExtras();
  }

  /* package */ boolean isLegacySession() {
    return impl.isLegacySession();
  }

  @Nullable
  /* package */ Object getBinder() {
    return impl.getBinder();
  }

  /**
   * Creates a token from {@link MediaSessionCompat.Token}.
   *
   * @return a {@link ListenableFuture} of {@link SessionToken}
   */
  @UnstableApi
  public static ListenableFuture<SessionToken> createSessionToken(
      Context context, Object compatToken) {
    checkNotNull(context, "context must not be null");
    checkNotNull(compatToken, "compatToken must not be null");
    checkArgument(compatToken instanceof MediaSessionCompat.Token);

    HandlerThread thread = new HandlerThread("SessionTokenThread");
    thread.start();

    SettableFuture<SessionToken> future = SettableFuture.create();
    // Try retrieving media3 token by connecting to the session.
    MediaControllerCompat controller =
        createMediaControllerCompat(context, (MediaSessionCompat.Token) compatToken);
    String packageName = controller.getPackageName();
    int uid = getUid(context.getPackageManager(), packageName);
    Handler handler = new Handler(thread.getLooper());
    controller.sendCommand(
        MediaConstants.SESSION_COMMAND_REQUEST_SESSION3_TOKEN,
        /* params= */ null,
        new ResultReceiver(handler) {
          @Override
          protected void onReceiveResult(int resultCode, Bundle resultData) {
            handler.removeCallbacksAndMessages(null);
            future.set(SessionToken.CREATOR.fromBundle(resultData));
          }
        });

    handler.postDelayed(
        () -> {
          // Timed out getting session3 token. Handle this as a legacy token.
          SessionToken resultToken =
              new SessionToken(
                  new SessionTokenImplLegacy(
                      (MediaSessionCompat.Token) compatToken,
                      packageName,
                      uid,
                      controller.getSessionInfo()));
          future.set(resultToken);
        },
        WAIT_TIME_MS_FOR_SESSION3_TOKEN);
    future.addListener(() -> thread.quit(), MoreExecutors.directExecutor());
    return future;
  }

  /**
   * Returns a {@link ImmutableSet} of {@link SessionToken} for media session services; {@link
   * MediaSessionService}, {@link MediaLibraryService}, and {@link MediaBrowserServiceCompat}
   * regardless of their activeness. This set represents media apps that publish {@link
   * MediaSession}.
   *
   * <p>The app targeting API level 30 or higher must include a {@code <queries>} element in their
   * manifest to get service tokens of other apps. See the following example and <a
   * href="//developer.android.com/training/package-visibility">this guide</a> for more information.
   *
   * <pre>{@code
   * <intent>
   *   <action android:name="androidx.media3.session.MediaSessionService" />
   * </intent>
   * <intent>
   *   <action android:name="androidx.media3.session.MediaLibraryService" />
   * </intent>
   * <intent>
   *   <action android:name="android.media.browse.MediaBrowserService" />
   * </intent>
   * }</pre>
   */
  public static ImmutableSet<SessionToken> getAllServiceTokens(Context context) {
    PackageManager pm = context.getPackageManager();
    List<ResolveInfo> services = new ArrayList<>();
    // If multiple actions are declared for a service, browser gets higher priority.
    List<ResolveInfo> libraryServices =
        pm.queryIntentServices(
            new Intent(MediaLibraryService.SERVICE_INTERFACE), PackageManager.GET_META_DATA);
    if (libraryServices != null) {
      services.addAll(libraryServices);
    }
    List<ResolveInfo> sessionServices =
        pm.queryIntentServices(
            new Intent(MediaSessionService.SERVICE_INTERFACE), PackageManager.GET_META_DATA);
    if (sessionServices != null) {
      services.addAll(sessionServices);
    }
    List<ResolveInfo> browserServices =
        pm.queryIntentServices(
            new Intent(MediaBrowserServiceCompat.SERVICE_INTERFACE), PackageManager.GET_META_DATA);
    if (browserServices != null) {
      services.addAll(browserServices);
    }

    ImmutableSet.Builder<SessionToken> sessionServiceTokens = ImmutableSet.builder();
    for (ResolveInfo service : services) {
      if (service == null || service.serviceInfo == null) {
        continue;
      }
      ServiceInfo serviceInfo = service.serviceInfo;
      SessionToken token =
          new SessionToken(context, new ComponentName(serviceInfo.packageName, serviceInfo.name));
      sessionServiceTokens.add(token);
    }
    return sessionServiceTokens.build();
  }

  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;
  }

  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);
  }

  /* package */ interface SessionTokenImpl extends Bundleable {

    boolean isLegacySession();

    int getUid();

    String getPackageName();

    String getServiceName();

    @Nullable
    ComponentName getComponentName();

    @TokenType
    int getType();

    int getSessionVersion();

    Bundle getExtras();

    @Nullable
    Object getBinder();
  }

  // Bundleable implementation.

  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @Target(TYPE_USE)
  @IntDef({FIELD_IMPL_TYPE, FIELD_IMPL})
  private @interface FieldNumber {}

  private static final int FIELD_IMPL_TYPE = 0;
  private static final int FIELD_IMPL = 1;

  /** Types of {@link SessionTokenImpl} */
  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @Target(TYPE_USE)
  @IntDef({IMPL_TYPE_BASE, IMPL_TYPE_LEGACY})
  private @interface SessionTokenImplType {}

  private static final int IMPL_TYPE_BASE = 0;
  private static final int IMPL_TYPE_LEGACY = 1;

  @UnstableApi
  @Override
  public Bundle toBundle() {
    Bundle bundle = new Bundle();
    if (impl instanceof SessionTokenImplBase) {
      bundle.putInt(keyForField(FIELD_IMPL_TYPE), IMPL_TYPE_BASE);
    } else {
      bundle.putInt(keyForField(FIELD_IMPL_TYPE), IMPL_TYPE_LEGACY);
    }
    bundle.putBundle(keyForField(FIELD_IMPL), impl.toBundle());
    return bundle;
  }

  /** Object that can restore {@link SessionToken} from a {@link Bundle}. */
  @UnstableApi public static final Creator<SessionToken> CREATOR = SessionToken::fromBundle;

  private static SessionToken fromBundle(Bundle bundle) {
    return new SessionToken(bundle);
  }

  private static String keyForField(@FieldNumber int field) {
    return Integer.toString(field, Character.MAX_RADIX);
  }
}