Add CarAudioFocus support for repeated focus requests
Test: Exercise KitchenSink on Mojave and drive Jeep
Bug: 33352354
Change-Id: I6aec30fe6714c0ea5124028442164d1f3fa9129b
diff --git a/service/src/com/android/car/CarAudioFocus.java b/service/src/com/android/car/CarAudioFocus.java
index c7c4e7c..a0552d9 100644
--- a/service/src/com/android/car/CarAudioFocus.java
+++ b/service/src/com/android/car/CarAudioFocus.java
@@ -96,6 +96,13 @@
// This is used both for focus dispatch and death handling
// Note that the clientId reflects the AudioManager instance and listener object (if any)
// so that one app can have more than one unique clientId by setting up distinct listeners.
+ // Because the listener gets only LOSS/GAIN messages, this is important for an app to do if
+ // it expects to request focus concurrently for different USAGEs so it knows which USAGE
+ // gained or lost focus at any given moment. If the SAME listener is used for requests of
+ // different USAGE while the earlier request is still in the focus stack (whether holding
+ // focus or pending), the new request will be REJECTED so as to avoid any confusion about
+ // the meaning of subsequent GAIN/LOSS events (which would continue to apply to the focus
+ // request that was already active or pending).
private HashMap<String, FocusEntry> mFocusHolders = new HashMap<String, FocusEntry>();
private HashMap<String, FocusEntry> mFocusLosers = new HashMap<String, FocusEntry>();
@@ -129,8 +136,13 @@
/** @see AudioManager#requestAudioFocus(AudioManager.OnAudioFocusChangeListener, int, int, int) */
- // TODO: How should we handle focus requests from clients who are already in the focus stack?
- // Especially if they're requesting with different AudioAttributes?
+ // Note that we replicate most, but not all of the behaviors of the default MediaFocusControl
+ // engine as of Android P.
+ // Besides the interaction matrix which allows concurrent focus for multiple requestors, which
+ // is the reason for this module, we also treat repeated requests from the same clientId
+ // slightly differently.
+ // If a focus request for the same listener (clientId) is received while that listener is
+ // already in the focus stack, we REJECT it outright unless it is for the same USAGE.
// The default audio framework's behavior is to remove the previous entry in the stack (no-op
// if the requester is already holding focus).
int evaluateFocusRequest(AudioFocusInfo afi) {
@@ -152,12 +164,17 @@
final int requestedContext = mCarAudioService.getContextForUsage(
afi.getAttributes().getUsage());
+ // If we happen find an entry that this new request should replace, we'll store it here.
+ FocusEntry deprecatedBlockedEntry = null;
// Scan all active and pending focus requests. If any should cause rejection of
// this new request, then we're done. Keep a list of those against whom we're exclusive
// so we can update the relationships if/when we are sure we won't get rejected.
+ Log.i(TAG, "Scanning focus holders...");
final ArrayList<FocusEntry> losers = new ArrayList<FocusEntry>();
for (FocusEntry entry : mFocusHolders.values()) {
+ Log.i(TAG, entry.mAfi.getClientId());
+
// If this request is for Notifications and a current focus holder has specified
// AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, then reject the request.
// This matches the hardwired behavior in the default audio policy engine which apps
@@ -169,6 +186,20 @@
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
}
+ // We don't allow sharing listeners (client IDs) between two concurrent requests
+ // (because the app would have no way to know to which request a later event applied)
+ if (afi.getClientId().equals(entry.mAfi.getClientId())) {
+ if (entry.mAudioContext == requestedContext) {
+ // Trivially accept if this request is a duplicate
+ Log.i(TAG, "Duplicate request from focus holder is accepted");
+ return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+ } else {
+ // Trivially reject a request for a different USAGE
+ Log.i(TAG, "Different request from focus holder is rejected");
+ return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+ }
+ }
+
// Check the interaction matrix for the relationship between this entry and the request
switch (sInteractionMatrix[entry.mAudioContext][requestedContext]) {
case INTERACTION_REJECT:
@@ -191,8 +222,11 @@
}
}
}
+ Log.i(TAG, "Scanning those who've already lost focus...");
final ArrayList<FocusEntry> blocked = new ArrayList<FocusEntry>();
for (FocusEntry entry : mFocusLosers.values()) {
+ Log.i(TAG, entry.mAfi.getClientId());
+
// If this request is for Notifications and a pending focus holder has specified
// AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, then reject the request
if ((requestedContext == ContextNumber.NOTIFICATION) &&
@@ -201,6 +235,24 @@
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
}
+ // We don't allow sharing listeners (client IDs) between two concurrent requests
+ // (because the app would have no way to know to which request a later event applied)
+ if (afi.getClientId().equals(entry.mAfi.getClientId())) {
+ if (entry.mAudioContext == requestedContext) {
+ // This is a repeat of a request that is currently blocked.
+ // Evaluate it as if it were a new request, but note that we should remove
+ // the old pending request, and move it.
+ // We do not want to evaluate the new request against itself.
+ Log.i(TAG, "Duplicate request while waiting is being evaluated");
+ deprecatedBlockedEntry = entry;
+ continue;
+ } else {
+ // Trivially reject a request for a different USAGE
+ Log.i(TAG, "Different request while waiting is rejected");
+ return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+ }
+ }
+
// Check the interaction matrix for the relationship between this entry and the request
switch (sInteractionMatrix[entry.mAudioContext][requestedContext]) {
case INTERACTION_REJECT:
@@ -262,9 +314,16 @@
}
}
+ // If we encountered a duplicate of this request that was pending, but now we're going to
+ // grant focus, we need to remove the old pending request (without sending a LOSS message).
+ if (deprecatedBlockedEntry != null) {
+ mFocusLosers.remove(deprecatedBlockedEntry.mAfi.getClientId());
+ }
+
// Finally, add the request we're granting to the focus holders' list
mFocusHolders.put(afi.getClientId(), newEntry);
+ Log.i(TAG, "AUDIOFOCUS_REQUEST_GRANTED");
return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
}