Precedence for app provided media button receiver

This change selects the best suited media button receiver
component and pending intent when creating the legacy
session. This is important to ensure that a service can
be started with a media button event from BT headsets
after the app has been terminated.

The `MediaSessionLegacyStub` selects the best suited
receiver to be passed to the `MediaSessionCompat`
constructor.

1. When the app has declared a broadcast receiver for
 `ACTION_MEDIA_BUTTON` in the manifest, this broadcast
 receiver is used.
2. When the session is housed in a service, the service
 component is used as a fallback.
3. As a last resort a receiver is created at runtime.

When the `MediaSessionLegacyStub` is released, the media
button receiver is removed unless the app has provided a
media button receiver in the manifest. In this case we
assume the app supports resuming when the BT play intent
arrives at `MediaSessionService.onStartCommand`.

#minor-release

Issue: androidx/media#167
Issue: androidx/media#27
Issue: androidx/media#314
PiperOrigin-RevId: 523638051
This commit is contained in:
bachinger 2023-04-12 10:00:03 +01:00 committed by Rohit Singh
parent ae875648a7
commit e54a934398
2 changed files with 107 additions and 70 deletions

View File

@ -17,18 +17,15 @@ package androidx.media3.session;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.common.util.Util.postOrRun;
import static androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED;
import static androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN;
import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
@ -42,7 +39,6 @@ import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.support.v4.media.session.MediaSessionCompat;
import android.view.KeyEvent;
import androidx.annotation.FloatRange;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
@ -66,7 +62,6 @@ import androidx.media3.common.VideoSize;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.BitmapLoader;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaSession.ControllerCb;
import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.session.MediaSession.MediaItemsWithStartPosition;
@ -115,8 +110,6 @@ import org.checkerframework.checker.initialization.qual.Initialized;
private final SessionToken sessionToken;
private final MediaSession instance;
@Nullable private final PendingIntent sessionActivity;
private final PendingIntent mediaButtonIntent;
@Nullable private final BroadcastReceiver broadcastReceiver;
private final Handler applicationHandler;
private final BitmapLoader bitmapLoader;
private final Runnable periodicSessionPositionInfoUpdateRunnable;
@ -189,52 +182,21 @@ import org.checkerframework.checker.initialization.qual.Initialized;
sessionStub,
tokenExtras);
@Nullable ComponentName mbrComponent;
synchronized (STATIC_LOCK) {
if (!componentNamesInitialized) {
serviceComponentName =
MediaSessionImpl.serviceComponentName =
getServiceComponentByAction(context, MediaLibraryService.SERVICE_INTERFACE);
if (serviceComponentName == null) {
serviceComponentName =
if (MediaSessionImpl.serviceComponentName == null) {
MediaSessionImpl.serviceComponentName =
getServiceComponentByAction(context, MediaSessionService.SERVICE_INTERFACE);
}
componentNamesInitialized = true;
}
mbrComponent = serviceComponentName;
}
int pendingIntentFlagMutable = Util.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0;
if (mbrComponent == null) {
// No service to revive playback after it's dead.
// Create a PendingIntent that points to the runtime broadcast receiver.
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, sessionUri);
intent.setPackage(context.getPackageName());
mediaButtonIntent =
PendingIntent.getBroadcast(
context, /* requestCode= */ 0, intent, pendingIntentFlagMutable);
// Creates a fake ComponentName for MediaSessionCompat in pre-L.
mbrComponent = new ComponentName(context, context.getClass());
// Create and register a BroadcastReceiver for receiving PendingIntent.
broadcastReceiver = new MediaButtonReceiver();
IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON);
filter.addDataScheme(castNonNull(sessionUri.getScheme()));
Util.registerReceiverNotExported(context, broadcastReceiver, filter);
} else {
// Has MediaSessionService to revive playback after it's dead.
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, sessionUri);
intent.setComponent(mbrComponent);
if (Util.SDK_INT >= 26) {
mediaButtonIntent =
PendingIntent.getForegroundService(context, 0, intent, pendingIntentFlagMutable);
} else {
mediaButtonIntent = PendingIntent.getService(context, 0, intent, pendingIntentFlagMutable);
}
broadcastReceiver = null;
}
sessionLegacyStub =
new MediaSessionLegacyStub(thisRef, mbrComponent, mediaButtonIntent, applicationHandler);
new MediaSessionLegacyStub(
/* session= */ thisRef, sessionUri, serviceComponentName, applicationHandler);
PlayerWrapper playerWrapper = new PlayerWrapper(player);
this.playerWrapper = playerWrapper;
@ -304,10 +266,6 @@ import org.checkerframework.checker.initialization.qual.Initialized;
Log.w(TAG, "Exception thrown while closing", e);
}
sessionLegacyStub.release();
mediaButtonIntent.cancel();
if (broadcastReceiver != null) {
context.unregisterReceiver(broadcastReceiver);
}
sessionStub.release();
}
@ -1285,26 +1243,6 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
}
// TODO(b/193193462): Replace this with androidx.media.session.MediaButtonReceiver
private final class MediaButtonReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
return;
}
Uri sessionUri = intent.getData();
if (!Util.areEqual(sessionUri, MediaSessionImpl.this.sessionUri)) {
return;
}
KeyEvent keyEvent = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (keyEvent == null) {
return;
}
getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
}
}
private class PlayerInfoChangedHandler extends Handler {
private static final int MSG_PLAYER_INFO_CHANGED = 1;

View File

@ -35,6 +35,7 @@ import static androidx.media3.common.Player.STATE_ENDED;
import static androidx.media3.common.Player.STATE_IDLE;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.common.util.Util.postOrRun;
import static androidx.media3.session.MediaUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_CUSTOM;
@ -43,9 +44,13 @@ import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED;
import static androidx.media3.session.SessionResult.RESULT_SUCCESS;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
@ -107,6 +112,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private static final String TAG = "MediaSessionLegacyStub";
private static final int PENDING_INTENT_FLAG_MUTABLE =
Util.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0;
private static final String DEFAULT_MEDIA_SESSION_TAG_PREFIX = "androidx.media3.session.id";
private static final String DEFAULT_MEDIA_SESSION_TAG_DELIM = ".";
@ -122,6 +129,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private final MediaPlayPauseKeyHandler mediaPlayPauseKeyHandler;
private final MediaSessionCompat sessionCompat;
private final String appPackageName;
@Nullable private final MediaButtonReceiver runtimeBroadcastReceiver;
private final boolean canResumePlaybackOnStart;
@Nullable private VolumeProviderCompat volumeProviderCompat;
private volatile long connectionTimeoutMs;
@ -130,8 +139,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
public MediaSessionLegacyStub(
MediaSessionImpl session,
ComponentName mbrComponent,
PendingIntent mediaButtonIntent,
Uri sessionUri,
@Nullable ComponentName serviceComponentName,
Handler handler) {
sessionImpl = session;
Context context = sessionImpl.getContext();
@ -145,6 +154,44 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
connectedControllersManager = new ConnectedControllersManager<>(session);
connectionTimeoutMs = DEFAULT_CONNECTION_TIMEOUT_MS;
// Select a media button receiver component.
ComponentName receiverComponentName = queryPackageManagerForMediaButtonReceiver(context);
// Assume an app that intentionally puts a `MediaButtonReceiver` into the manifest has
// implemented some kind of resumption of the last recently played media item.
canResumePlaybackOnStart = receiverComponentName != null;
if (receiverComponentName == null) {
receiverComponentName = serviceComponentName;
}
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, sessionUri);
PendingIntent mediaButtonIntent;
if (receiverComponentName == null) {
// Neither a media button receiver from the app manifest nor a service available that could
// handle media button events. Create a runtime receiver and a pending intent for it.
runtimeBroadcastReceiver = new MediaButtonReceiver();
IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON);
filter.addDataScheme(castNonNull(sessionUri.getScheme()));
Util.registerReceiverNotExported(context, runtimeBroadcastReceiver, filter);
// Create a pending intent to be broadcast to the receiver.
intent.setPackage(context.getPackageName());
mediaButtonIntent =
PendingIntent.getBroadcast(
context, /* requestCode= */ 0, intent, PENDING_INTENT_FLAG_MUTABLE);
// Creates a fake ComponentName for MediaSessionCompat in pre-L.
receiverComponentName = new ComponentName(context, context.getClass());
} else {
intent.setComponent(receiverComponentName);
mediaButtonIntent =
Objects.equals(serviceComponentName, receiverComponentName)
? (Util.SDK_INT >= 26
? PendingIntent.getForegroundService(
context, /* requestCode= */ 0, intent, PENDING_INTENT_FLAG_MUTABLE)
: PendingIntent.getService(
context, /* requestCode= */ 0, intent, PENDING_INTENT_FLAG_MUTABLE))
: PendingIntent.getBroadcast(
context, /* requestCode= */ 0, intent, PENDING_INTENT_FLAG_MUTABLE);
runtimeBroadcastReceiver = null;
}
String sessionCompatId =
TextUtils.join(
DEFAULT_MEDIA_SESSION_TAG_DELIM,
@ -153,7 +200,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
new MediaSessionCompat(
context,
sessionCompatId,
mbrComponent,
receiverComponentName,
mediaButtonIntent,
session.getToken().getExtras());
@ -168,12 +215,38 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
sessionCompat.setCallback(thisRef, handler);
}
@Nullable
private static ComponentName queryPackageManagerForMediaButtonReceiver(Context context) {
PackageManager pm = context.getPackageManager();
Intent queryIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
queryIntent.setPackage(context.getPackageName());
List<ResolveInfo> resolveInfos = pm.queryBroadcastReceivers(queryIntent, /* flags= */ 0);
if (resolveInfos.size() == 1) {
ResolveInfo resolveInfo = resolveInfos.get(0);
return new ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name);
} else if (resolveInfos.isEmpty()) {
return null;
} else {
throw new IllegalStateException(
"Expected 1 broadcast receiver that handles "
+ Intent.ACTION_MEDIA_BUTTON
+ ", found "
+ resolveInfos.size());
}
}
/** Starts to receive commands. */
public void start() {
sessionCompat.setActive(true);
}
public void release() {
if (!canResumePlaybackOnStart) {
setMediaButtonReceiver(sessionCompat, /* mediaButtonReceiverIntent= */ null);
}
if (runtimeBroadcastReceiver != null) {
sessionImpl.getContext().unregisterReceiver(runtimeBroadcastReceiver);
}
// No check for COMMAND_RELEASE needed as MediaControllers can always be released.
sessionCompat.release();
}
@ -833,6 +906,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
sessionCompat.setMetadata(metadataCompat);
}
@SuppressWarnings("nullness:argument") // MediaSessionCompat didn't annotate @Nullable.
private static void setMediaButtonReceiver(
MediaSessionCompat sessionCompat, @Nullable PendingIntent mediaButtonReceiverIntent) {
sessionCompat.setMediaButtonReceiver(mediaButtonReceiverIntent);
}
@SuppressWarnings("nullness:argument") // MediaSessionCompat didn't annotate @Nullable.
private static void setQueue(MediaSessionCompat sessionCompat, @Nullable List<QueueItem> queue) {
sessionCompat.setQueue(queue);
@ -1359,4 +1438,24 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private static String getBitmapLoadErrorMessage(Throwable throwable) {
return "Failed to load bitmap: " + throwable.getMessage();
}
// TODO(b/193193462): Replace this with androidx.media.session.MediaButtonReceiver
private final class MediaButtonReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!Util.areEqual(intent.getAction(), Intent.ACTION_MEDIA_BUTTON)) {
return;
}
Uri sessionUri = intent.getData();
if (!Util.areEqual(sessionUri, sessionUri)) {
return;
}
KeyEvent keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (keyEvent == null) {
return;
}
getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
}
}
}