Skip to content

Commit

Permalink
Precedence for app provided media button receiver
Browse files Browse the repository at this point in the history
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`.

Issue: #167
Issue: #27
Issue: #314
PiperOrigin-RevId: 523638051
(cherry picked from commit e54a934)
  • Loading branch information
marcbaechinger authored and rohitjoins committed Apr 18, 2023
1 parent 9360530 commit eb322b7
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,15 @@

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;
Expand All @@ -42,7 +39,6 @@
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;
Expand All @@ -65,7 +61,6 @@
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.CueGroup;
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;
Expand Down Expand Up @@ -114,8 +109,6 @@
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;
Expand Down Expand Up @@ -188,52 +181,21 @@ public MediaSessionImpl(
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;
Expand Down Expand Up @@ -303,10 +265,6 @@ public void release() {
Log.w(TAG, "Exception thrown while closing", e);
}
sessionLegacyStub.release();
mediaButtonIntent.cancel();
if (broadcastReceiver != null) {
context.unregisterReceiver(broadcastReceiver);
}
sessionStub.release();
}

Expand Down Expand Up @@ -1284,26 +1242,6 @@ private MediaSessionImpl getSession() {
}
}

// 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
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;
Expand All @@ -43,9 +44,13 @@
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;
Expand Down Expand Up @@ -107,6 +112,8 @@

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 = ".";

Expand All @@ -122,6 +129,8 @@
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;
Expand All @@ -130,8 +139,8 @@

public MediaSessionLegacyStub(
MediaSessionImpl session,
ComponentName mbrComponent,
PendingIntent mediaButtonIntent,
Uri sessionUri,
@Nullable ComponentName serviceComponentName,
Handler handler) {
sessionImpl = session;
Context context = sessionImpl.getContext();
Expand All @@ -145,6 +154,44 @@ public MediaSessionLegacyStub(
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,
Expand All @@ -153,7 +200,7 @@ public MediaSessionLegacyStub(
new MediaSessionCompat(
context,
sessionCompatId,
mbrComponent,
receiverComponentName,
mediaButtonIntent,
session.getToken().getExtras());

Expand All @@ -168,12 +215,38 @@ public MediaSessionLegacyStub(
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);
}
sessionCompat.release();
}

Expand Down Expand Up @@ -832,6 +905,12 @@ private static void setMetadata(
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);
Expand Down Expand Up @@ -1358,4 +1437,24 @@ public boolean hasPendingMediaPlayPauseKey() {
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);
}
}
}

0 comments on commit eb322b7

Please sign in to comment.