Use Futures for MediaSession command queue instead of Runnables
Some commands may be asynchronous and subsequent commands need to wait for them to complete before running. This change updates the queue to use (and listen to) Futures instead of calling Runnables directly. The commands are currently still added as Runanbles though, so this change is a no-op. Also moves the permission check in MediaSessionImpl to before queueing the command because the permission should be check at the time of calling the method. When executing the comamnds in the queue, we need to be careful to avoid recursion in the same thread (which happens when both the Future is immediate and running on the correct thread already). To avoid recursion, we detect this case and loop the commands instead. Issue: androidx/media#85 PiperOrigin-RevId: 461827264
This commit is contained in:
parent
ba9c9bb964
commit
dee80788e4
@ -24,8 +24,12 @@ import androidx.collection.ArrayMap;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.session.MediaSession.ControllerInfo;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
@ -39,6 +43,17 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
*/
|
||||
/* package */ final class ConnectedControllersManager<T extends @NonNull Object> {
|
||||
|
||||
/** An asynchronous controller command function. */
|
||||
public interface AsyncCommand {
|
||||
|
||||
/**
|
||||
* Runs the asynchronous command.
|
||||
*
|
||||
* @return A {@link ListenableFuture} to listen for the command completion.
|
||||
*/
|
||||
ListenableFuture<Void> run();
|
||||
}
|
||||
|
||||
private final Object lock;
|
||||
|
||||
@GuardedBy("lock")
|
||||
@ -213,34 +228,69 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
}
|
||||
|
||||
public void addToCommandQueue(ControllerInfo controllerInfo, Runnable commandRunnable) {
|
||||
@Nullable ConnectedControllerRecord<T> info;
|
||||
synchronized (lock) {
|
||||
info = controllerRecords.get(controllerInfo);
|
||||
}
|
||||
if (info != null) {
|
||||
info.commandQueue.add(commandRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
public Deque<Runnable> getAndClearCommandQueue(ControllerInfo controllerInfo) {
|
||||
Deque<Runnable> commandQueue = new ArrayDeque<>();
|
||||
synchronized (lock) {
|
||||
@Nullable ConnectedControllerRecord<T> info = controllerRecords.get(controllerInfo);
|
||||
if (info != null) {
|
||||
commandQueue.addAll(info.commandQueue);
|
||||
info.commandQueue.clear();
|
||||
info.commandQueue.add(
|
||||
() -> {
|
||||
commandRunnable.run();
|
||||
return Futures.immediateVoidFuture();
|
||||
});
|
||||
}
|
||||
}
|
||||
return commandQueue;
|
||||
}
|
||||
|
||||
public void flushCommandQueue(ControllerInfo controllerInfo) {
|
||||
synchronized (lock) {
|
||||
@Nullable ConnectedControllerRecord<T> info = controllerRecords.get(controllerInfo);
|
||||
if (info == null || info.commandQueueIsFlushing || info.commandQueue.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
info.commandQueueIsFlushing = true;
|
||||
flushCommandQueue(info);
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void flushCommandQueue(ConnectedControllerRecord<T> info) {
|
||||
AtomicBoolean continueRunning = new AtomicBoolean(true);
|
||||
while (continueRunning.get()) {
|
||||
continueRunning.set(false);
|
||||
@Nullable AsyncCommand asyncCommand = info.commandQueue.poll();
|
||||
if (asyncCommand == null) {
|
||||
info.commandQueueIsFlushing = false;
|
||||
return;
|
||||
}
|
||||
AtomicBoolean commandExecuting = new AtomicBoolean(true);
|
||||
postOrRun(
|
||||
sessionImpl.getApplicationHandler(),
|
||||
() ->
|
||||
asyncCommand
|
||||
.run()
|
||||
.addListener(
|
||||
() -> {
|
||||
synchronized (lock) {
|
||||
if (!commandExecuting.get()) {
|
||||
flushCommandQueue(info);
|
||||
} else {
|
||||
continueRunning.set(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor()));
|
||||
commandExecuting.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ConnectedControllerRecord<T> {
|
||||
|
||||
public final T controllerKey;
|
||||
public final SequencedFutureManager sequencedFutureManager;
|
||||
public final Deque<AsyncCommand> commandQueue;
|
||||
|
||||
public SessionCommands sessionCommands;
|
||||
public Player.Commands playerCommands;
|
||||
public Deque<Runnable> commandQueue;
|
||||
public boolean commandQueueIsFlushing;
|
||||
|
||||
public ConnectedControllerRecord(
|
||||
T controllerKey,
|
||||
|
@ -81,7 +81,6 @@ import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@ -225,30 +224,9 @@ import java.util.concurrent.ExecutionException;
|
||||
if (controller == null) {
|
||||
return;
|
||||
}
|
||||
if (command == COMMAND_SET_VIDEO_SURFACE) {
|
||||
postOrRun(
|
||||
sessionImpl.getApplicationHandler(),
|
||||
getSessionTaskWithPlayerCommandRunnable(
|
||||
controller, seq, command, sessionImpl, task, postTask));
|
||||
} else {
|
||||
connectedControllersManager.addToCommandQueue(
|
||||
controller,
|
||||
getSessionTaskWithPlayerCommandRunnable(
|
||||
controller, seq, command, sessionImpl, task, postTask));
|
||||
}
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
private <T, K extends MediaSessionImpl> Runnable getSessionTaskWithPlayerCommandRunnable(
|
||||
ControllerInfo controller,
|
||||
int seq,
|
||||
@Player.Command int command,
|
||||
K sessionImpl,
|
||||
SessionTask<T, K> task,
|
||||
PostSessionTask<T, K> postTask) {
|
||||
return () -> {
|
||||
() -> {
|
||||
if (!connectedControllersManager.isPlayerCommandAvailable(controller, command)) {
|
||||
sendSessionResult(
|
||||
sessionImpl,
|
||||
@ -264,6 +242,28 @@ import java.util.concurrent.ExecutionException;
|
||||
sendSessionResult(sessionImpl, controller, seq, new SessionResult(resultCode));
|
||||
return;
|
||||
}
|
||||
if (command == COMMAND_SET_VIDEO_SURFACE) {
|
||||
getSessionTaskWithPlayerCommandRunnable(controller, seq, sessionImpl, task, postTask)
|
||||
.run();
|
||||
} else {
|
||||
connectedControllersManager.addToCommandQueue(
|
||||
controller,
|
||||
getSessionTaskWithPlayerCommandRunnable(
|
||||
controller, seq, sessionImpl, task, postTask));
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
private <T, K extends MediaSessionImpl> Runnable getSessionTaskWithPlayerCommandRunnable(
|
||||
ControllerInfo controller,
|
||||
int seq,
|
||||
K sessionImpl,
|
||||
SessionTask<T, K> task,
|
||||
PostSessionTask<T, K> postTask) {
|
||||
return () -> {
|
||||
T result = task.run(sessionImpl, controller);
|
||||
postTask.run(sessionImpl, controller, seq, result);
|
||||
};
|
||||
@ -1450,17 +1450,9 @@ import java.util.concurrent.ExecutionException;
|
||||
}
|
||||
ControllerInfo controllerInfo = connectedControllersManager.getController(caller.asBinder());
|
||||
if (controllerInfo != null) {
|
||||
Deque<Runnable> queue = connectedControllersManager.getAndClearCommandQueue(controllerInfo);
|
||||
postOrRun(
|
||||
sessionImpl.getApplicationHandler(),
|
||||
() -> {
|
||||
while (!queue.isEmpty()) {
|
||||
Runnable runnable = queue.poll();
|
||||
if (runnable != null) {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
});
|
||||
() -> connectedControllersManager.flushCommandQueue(controllerInfo));
|
||||
}
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
|
Loading…
x
Reference in New Issue
Block a user