Commit ee209690 by tonihei Committed by Rohit Singh

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
parent 287c7579
...@@ -25,6 +25,8 @@ ...@@ -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)
......
...@@ -320,7 +320,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; ...@@ -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; ...@@ -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;
private final Handler handler;
public FlushCommandQueueHandler(Looper looper) { public FlushCommandQueueHandler(Looper looper) {
super(looper); handler = new Handler(looper, /* callback= */ this::handleMessage);
} }
@Override public void sendFlushCommandQueueMessage() {
public void handleMessage(Message msg) { 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.
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) { if (msg.what == MSG_FLUSH_COMMAND_QUEUE) {
try { flushCommandQueue();
iSession.flushCommandQueue(controllerStub);
} catch (RemoteException e) {
Log.w(TAG, "Error in sending flushCommandQueue");
}
} }
return true;
} }
public void sendFlushCommandQueueMessage() { private void flushCommandQueue() {
if (iSession != null && !hasMessages(MSG_FLUSH_COMMAND_QUEUE)) { try {
// Send message to notify the end of the transaction. It will be handled when the current iSession.flushCommandQueue(controllerStub);
// looper iteration is over. } catch (RemoteException e) {
sendEmptyMessage(MSG_FLUSH_COMMAND_QUEUE); Log.w(TAG, "Error in sending flushCommandQueue");
} }
} }
} }
......
...@@ -552,7 +552,13 @@ import java.util.concurrent.ExecutionException; ...@@ -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);
} }
......
...@@ -225,4 +225,27 @@ public class MediaSessionAndControllerTest { ...@@ -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);
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment