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 channel name used by the provider. Also, add method
`DefaultNotificationProvider.setSmallIcon(int)` to set the notifications `DefaultNotificationProvider.setSmallIcon(int)` to set the notifications
small icon ([#104](https://github.com/androidx/media/issues/104)). 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) ### 1.0.0-beta02 (2022-07-15)

View File

@ -320,7 +320,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
serviceConnection = null; serviceConnection = null;
} }
connectedToken = null; connectedToken = null;
flushCommandQueueHandler.removeCallbacksAndMessages(/* token= */ null); flushCommandQueueHandler.release();
this.iSession = null; this.iSession = null;
controllerStub.destroy(); controllerStub.destroy();
if (iSession != null) { 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; private static final int MSG_FLUSH_COMMAND_QUEUE = 1;
public FlushCommandQueueHandler(Looper looper) { private final Handler handler;
super(looper);
}
@Override public FlushCommandQueueHandler(Looper looper) {
public void handleMessage(Message msg) { handler = new Handler(looper, /* callback= */ this::handleMessage);
if (msg.what == MSG_FLUSH_COMMAND_QUEUE) {
try {
iSession.flushCommandQueue(controllerStub);
} catch (RemoteException e) {
Log.w(TAG, "Error in sending flushCommandQueue");
}
}
} }
public void sendFlushCommandQueueMessage() { 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 // Send message to notify the end of the transaction. It will be handled when the current
// looper iteration is over. // 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(); long token = Binder.clearCallingIdentity();
try { 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 { } finally {
Binder.restoreCallingIdentity(token); Binder.restoreCallingIdentity(token);
} }

View File

@ -225,4 +225,27 @@ public class MediaSessionAndControllerTest {
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(playWhenReadyRef.get()).isEqualTo(testPlayWhenReady); 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);
}
} }