Ensure pending commands are still sent in MediaController.release()

We currently clear all pending messages, including the one that flushes
pending commands to the MediaSession. To ensure all commands that have
been called before controller.release() are still sent, we can manually
trigger the flush message from the release call.

Related to handling the final flush because disconnecting the controller,
MediaSessionStub didn't post the removal of the controller to the
session thread, creating a race condition between removing the controller
and actually handling the flush.

Issue: androidx/media#99
PiperOrigin-RevId: 462342860
This commit is contained in:
tonihei 2022-07-21 10:10:22 +00:00 committed by Rohit Singh
parent 287c757944
commit ee209690cb
4 changed files with 61 additions and 17 deletions

View File

@ -25,6 +25,8 @@
channel name used by the provider. Also, add method
`DefaultNotificationProvider.setSmallIcon(int)` to set the notifications
small icon ([#104](https://github.com/androidx/media/issues/104)).
* Ensure commands sent before `MediaController.release()` are not dropped
([#99](https://github.com/androidx/media/issues/99)).
### 1.0.0-beta02 (2022-07-15)

View File

@ -320,7 +320,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
serviceConnection = null;
}
connectedToken = null;
flushCommandQueueHandler.removeCallbacksAndMessages(/* token= */ null);
flushCommandQueueHandler.release();
this.iSession = null;
controllerStub.destroy();
if (iSession != null) {
@ -3070,30 +3070,43 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}
}
private class FlushCommandQueueHandler extends Handler {
private class FlushCommandQueueHandler {
private static final int MSG_FLUSH_COMMAND_QUEUE = 1;
public FlushCommandQueueHandler(Looper looper) {
super(looper);
}
private final Handler handler;
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_FLUSH_COMMAND_QUEUE) {
try {
iSession.flushCommandQueue(controllerStub);
} catch (RemoteException e) {
Log.w(TAG, "Error in sending flushCommandQueue");
}
}
public FlushCommandQueueHandler(Looper looper) {
handler = new Handler(looper, /* callback= */ this::handleMessage);
}
public void sendFlushCommandQueueMessage() {
if (iSession != null && !hasMessages(MSG_FLUSH_COMMAND_QUEUE)) {
if (iSession != null && !handler.hasMessages(MSG_FLUSH_COMMAND_QUEUE)) {
// Send message to notify the end of the transaction. It will be handled when the current
// looper iteration is over.
sendEmptyMessage(MSG_FLUSH_COMMAND_QUEUE);
handler.sendEmptyMessage(MSG_FLUSH_COMMAND_QUEUE);
}
}
public void release() {
if (handler.hasMessages(MSG_FLUSH_COMMAND_QUEUE)) {
flushCommandQueue();
}
handler.removeCallbacksAndMessages(/* token= */ null);
}
private boolean handleMessage(Message msg) {
if (msg.what == MSG_FLUSH_COMMAND_QUEUE) {
flushCommandQueue();
}
return true;
}
private void flushCommandQueue() {
try {
iSession.flushCommandQueue(controllerStub);
} catch (RemoteException e) {
Log.w(TAG, "Error in sending flushCommandQueue");
}
}
}

View File

@ -552,7 +552,13 @@ import java.util.concurrent.ExecutionException;
}
long token = Binder.clearCallingIdentity();
try {
connectedControllersManager.removeController(caller.asBinder());
@Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get();
if (sessionImpl == null || sessionImpl.isReleased()) {
return;
}
postOrRun(
sessionImpl.getApplicationHandler(),
() -> connectedControllersManager.removeController(caller.asBinder()));
} finally {
Binder.restoreCallingIdentity(token);
}

View File

@ -225,4 +225,27 @@ public class MediaSessionAndControllerTest {
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(playWhenReadyRef.get()).isEqualTo(testPlayWhenReady);
}
@Test
public void commandBeforeControllerRelease_handledBySession() throws Exception {
MockPlayer player =
new MockPlayer.Builder().setApplicationLooper(Looper.getMainLooper()).build();
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setId(TAG).build());
MediaController controller = controllerTestRule.createController(session.getToken());
threadTestRule
.getHandler()
.postAndSync(
() -> {
controller.prepare();
controller.play();
controller.release();
});
// Assert these methods are called without timing out.
player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS);
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
}
}