blob: 719046c036f0ff6ecdacdaed533aebc33e727d69 [file] [log] [blame]
Scott Randolph54892f82018-03-02 13:24:55 -08001/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
Scott Randolph12edb602018-11-28 18:57:09 -080016package com.android.car.audio;
Scott Randolph54892f82018-03-02 13:24:55 -080017
18import android.hardware.automotive.audiocontrol.V1_0.ContextNumber;
19import android.media.AudioAttributes;
20import android.media.AudioFocusInfo;
21import android.media.AudioManager;
22import android.media.audiopolicy.AudioPolicy;
23import android.util.Log;
24
25import java.io.PrintWriter;
26import java.util.ArrayList;
27import java.util.HashMap;
28import java.util.Iterator;
29
30
31public class CarAudioFocus extends AudioPolicy.AudioPolicyFocusListener {
32
33 private static final String TAG = "CarAudioFocus";
34
35 private final AudioManager mAudioManager;
36 private CarAudioService mCarAudioService; // Dynamically assigned just after construction
37 private AudioPolicy mAudioPolicy; // Dynamically assigned just after construction
38
39
40 // Values for the internal interaction matrix we use to make focus decisions
41 private static final int INTERACTION_REJECT = 0; // Focus not granted
42 private static final int INTERACTION_EXCLUSIVE = 1; // Focus granted, others loose focus
43 private static final int INTERACTION_CONCURRENT = 2; // Focus granted, others keep focus
44
45 // TODO: Make this an overlayable resource...
46 // MUSIC = 1, // Music playback
47 // NAVIGATION = 2, // Navigation directions
48 // VOICE_COMMAND = 3, // Voice command session
49 // CALL_RING = 4, // Voice call ringing
50 // CALL = 5, // Voice call
51 // ALARM = 6, // Alarm sound from Android
52 // NOTIFICATION = 7, // Notifications
53 // SYSTEM_SOUND = 8, // User interaction sounds (button clicks, etc)
54 private static int sInteractionMatrix[][] = {
55 // Row selected by playing sound (labels along the right)
56 // Column selected by incoming request (labels along the top)
57 // Cell value is one of INTERACTION_REJECT, INTERACTION_EXCLUSIVE, INTERACTION_CONCURRENT
58 // Invalid, Music, Nav, Voice, Ring, Call, Alarm, Notification, System
59 { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Invalid
60 { 0, 1, 2, 1, 1, 1, 1, 2, 2 }, // Music
61 { 0, 2, 2, 1, 2, 1, 2, 2, 2 }, // Nav
62 { 0, 2, 0, 2, 1, 1, 0, 0, 0 }, // Voice
63 { 0, 0, 2, 2, 2, 2, 0, 0, 2 }, // Ring
64 { 0, 0, 2, 0, 2, 2, 2, 2, 0 }, // Context
65 { 0, 2, 2, 1, 1, 1, 2, 2, 2 }, // Alarm
66 { 0, 2, 2, 1, 1, 1, 2, 2, 2 }, // Notification
67 { 0, 2, 2, 1, 1, 1, 2, 2, 2 }, // System
68 };
69
70
71 private class FocusEntry {
72 // Requester info
73 final AudioFocusInfo mAfi; // never null
74
75 final int mAudioContext; // Which HAL level context does this affect
76 final ArrayList<FocusEntry> mBlockers; // List of requests that block ours
77
78 FocusEntry(AudioFocusInfo afi,
79 int context) {
80 mAfi = afi;
81 mAudioContext = context;
82 mBlockers = new ArrayList<FocusEntry>();
83 }
84
85 public String getClientId() {
86 return mAfi.getClientId();
87 }
88
89 public boolean wantsPauseInsteadOfDucking() {
90 return (mAfi.getFlags() & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0;
91 }
92 }
93
94
95 // We keep track of all the focus requesters in this map, with their clientId as the key.
96 // This is used both for focus dispatch and death handling
97 // Note that the clientId reflects the AudioManager instance and listener object (if any)
98 // so that one app can have more than one unique clientId by setting up distinct listeners.
Scott Randolph8eb49552018-10-17 00:56:39 -070099 // Because the listener gets only LOSS/GAIN messages, this is important for an app to do if
100 // it expects to request focus concurrently for different USAGEs so it knows which USAGE
101 // gained or lost focus at any given moment. If the SAME listener is used for requests of
102 // different USAGE while the earlier request is still in the focus stack (whether holding
103 // focus or pending), the new request will be REJECTED so as to avoid any confusion about
104 // the meaning of subsequent GAIN/LOSS events (which would continue to apply to the focus
105 // request that was already active or pending).
Scott Randolph54892f82018-03-02 13:24:55 -0800106 private HashMap<String, FocusEntry> mFocusHolders = new HashMap<String, FocusEntry>();
107 private HashMap<String, FocusEntry> mFocusLosers = new HashMap<String, FocusEntry>();
108
109
110 CarAudioFocus(AudioManager audioManager) {
111 mAudioManager = audioManager;
112 }
113
114
115 // This has to happen after the construction to avoid a chicken and egg problem when setting up
116 // the AudioPolicy which must depend on this object.
117 public void setOwningPolicy(CarAudioService audioService, AudioPolicy parentPolicy) {
118 mCarAudioService = audioService;
119 mAudioPolicy = parentPolicy;
120 }
121
122
123 // This sends a focus loss message to the targeted requester.
124 private void sendFocusLoss(FocusEntry loser, boolean permanent) {
125 int lossType = (permanent ? AudioManager.AUDIOFOCUS_LOSS :
126 AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
127 Log.i(TAG, "sendFocusLoss to " + loser.getClientId());
128 int result = mAudioManager.dispatchAudioFocusChange(loser.mAfi, lossType, mAudioPolicy);
129 if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
130 // TODO: Is this actually an error, or is it okay for an entry in the focus stack
131 // to NOT have a listener? If that's the case, should we even keep it in the focus
132 // stack?
133 Log.e(TAG, "Failure to signal loss of audio focus with error: " + result);
134 }
135 }
136
137
138 /** @see AudioManager#requestAudioFocus(AudioManager.OnAudioFocusChangeListener, int, int, int) */
Scott Randolph8eb49552018-10-17 00:56:39 -0700139 // Note that we replicate most, but not all of the behaviors of the default MediaFocusControl
140 // engine as of Android P.
141 // Besides the interaction matrix which allows concurrent focus for multiple requestors, which
142 // is the reason for this module, we also treat repeated requests from the same clientId
143 // slightly differently.
144 // If a focus request for the same listener (clientId) is received while that listener is
145 // already in the focus stack, we REJECT it outright unless it is for the same USAGE.
Justin Pauporeb11a8072019-03-12 20:03:25 -0700146 // If it is for the same USAGE, we replace the old request with the new one.
Scott Randolph54892f82018-03-02 13:24:55 -0800147 // The default audio framework's behavior is to remove the previous entry in the stack (no-op
148 // if the requester is already holding focus).
149 int evaluateFocusRequest(AudioFocusInfo afi) {
150 Log.i(TAG, "Evaluating focus request for client " + afi.getClientId());
151
152 // Is this a request for premanant focus?
153 // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -- Means Notifications should be denied
154 // AUDIOFOCUS_GAIN_TRANSIENT -- Means current focus holders should get transient loss
155 // AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -- Means other can duck (no loss message from us)
156 // NOTE: We expect that in practice it will be permanent for all media requests and
157 // transient for everything else, but that isn't currently an enforced requirement.
158 final boolean permanent =
159 (afi.getGainRequest() == AudioManager.AUDIOFOCUS_GAIN);
160 final boolean allowDucking =
161 (afi.getGainRequest() == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
162
163
164 // Convert from audio attributes "usage" to HAL level "context"
165 final int requestedContext = mCarAudioService.getContextForUsage(
166 afi.getAttributes().getUsage());
167
Justin Pauporeb11a8072019-03-12 20:03:25 -0700168 // If we happen to find entries that this new request should replace, we'll store them here.
169 // This happens when a client makes a second AF request on the same listener.
170 // After we've granted audio focus to our current request, we'll abandon these requests.
171 FocusEntry replacedCurrentEntry = null;
172 FocusEntry replacedBlockedEntry = null;
Scott Randolph54892f82018-03-02 13:24:55 -0800173
174 // Scan all active and pending focus requests. If any should cause rejection of
175 // this new request, then we're done. Keep a list of those against whom we're exclusive
176 // so we can update the relationships if/when we are sure we won't get rejected.
Scott Randolph8eb49552018-10-17 00:56:39 -0700177 Log.i(TAG, "Scanning focus holders...");
Scott Randolph54892f82018-03-02 13:24:55 -0800178 final ArrayList<FocusEntry> losers = new ArrayList<FocusEntry>();
179 for (FocusEntry entry : mFocusHolders.values()) {
Justin Pauporeb11a8072019-03-12 20:03:25 -0700180 Log.d(TAG, "Evaluating focus holder: " + entry.getClientId());
Scott Randolph8eb49552018-10-17 00:56:39 -0700181
Scott Randolph54892f82018-03-02 13:24:55 -0800182 // If this request is for Notifications and a current focus holder has specified
183 // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, then reject the request.
184 // This matches the hardwired behavior in the default audio policy engine which apps
185 // might expect (The interaction matrix doesn't have any provision for dealing with
186 // override flags like this).
187 if ((requestedContext == ContextNumber.NOTIFICATION) &&
188 (entry.mAfi.getGainRequest() ==
189 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)) {
190 return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
191 }
192
Scott Randolph8eb49552018-10-17 00:56:39 -0700193 // We don't allow sharing listeners (client IDs) between two concurrent requests
194 // (because the app would have no way to know to which request a later event applied)
195 if (afi.getClientId().equals(entry.mAfi.getClientId())) {
196 if (entry.mAudioContext == requestedContext) {
Justin Pauporeb11a8072019-03-12 20:03:25 -0700197 // This is a request from a current focus holder.
198 // Abandon the previous request (without sending a LOSS notification to it),
199 // and don't check the interaction matrix for it.
200 Log.i(TAG, "Replacing accepted request from same client");
201 replacedCurrentEntry = entry;
202 continue;
Scott Randolph8eb49552018-10-17 00:56:39 -0700203 } else {
204 // Trivially reject a request for a different USAGE
Justin Pauporeb11a8072019-03-12 20:03:25 -0700205 Log.e(TAG, "Client " + entry.getClientId() + " has already requested focus "
206 + "for " + entry.mAfi.getAttributes().usageToString() + " - cannot "
207 + "request focus for " + afi.getAttributes().usageToString() + " on "
208 + "same listener.");
Scott Randolph8eb49552018-10-17 00:56:39 -0700209 return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
210 }
211 }
212
Scott Randolph54892f82018-03-02 13:24:55 -0800213 // Check the interaction matrix for the relationship between this entry and the request
214 switch (sInteractionMatrix[entry.mAudioContext][requestedContext]) {
215 case INTERACTION_REJECT:
216 // This request is rejected, so nothing further to do
217 return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
218 case INTERACTION_EXCLUSIVE:
219 // The new request will cause this existing entry to lose focus
220 losers.add(entry);
221 break;
222 default:
223 // If ducking isn't allowed by the focus requestor, then everybody else
224 // must get a LOSS.
225 // If a focus holder has set the AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS flag,
226 // they must get a LOSS message even if ducking would otherwise be allowed.
Justin Pauporeb11a8072019-03-12 20:03:25 -0700227 if (!allowDucking || entry.wantsPauseInsteadOfDucking()) {
Scott Randolph54892f82018-03-02 13:24:55 -0800228 // The new request will cause audio book to lose focus and pause
229 losers.add(entry);
230 }
231 }
232 }
Scott Randolph8eb49552018-10-17 00:56:39 -0700233 Log.i(TAG, "Scanning those who've already lost focus...");
Scott Randolph54892f82018-03-02 13:24:55 -0800234 final ArrayList<FocusEntry> blocked = new ArrayList<FocusEntry>();
235 for (FocusEntry entry : mFocusLosers.values()) {
Scott Randolph8eb49552018-10-17 00:56:39 -0700236 Log.i(TAG, entry.mAfi.getClientId());
237
Scott Randolph54892f82018-03-02 13:24:55 -0800238 // If this request is for Notifications and a pending focus holder has specified
239 // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, then reject the request
240 if ((requestedContext == ContextNumber.NOTIFICATION) &&
241 (entry.mAfi.getGainRequest() ==
242 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)) {
243 return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
244 }
245
Scott Randolph8eb49552018-10-17 00:56:39 -0700246 // We don't allow sharing listeners (client IDs) between two concurrent requests
247 // (because the app would have no way to know to which request a later event applied)
248 if (afi.getClientId().equals(entry.mAfi.getClientId())) {
249 if (entry.mAudioContext == requestedContext) {
250 // This is a repeat of a request that is currently blocked.
251 // Evaluate it as if it were a new request, but note that we should remove
252 // the old pending request, and move it.
253 // We do not want to evaluate the new request against itself.
Justin Pauporeb11a8072019-03-12 20:03:25 -0700254 Log.i(TAG, "Replacing pending request from same client");
255 replacedBlockedEntry = entry;
Scott Randolph8eb49552018-10-17 00:56:39 -0700256 continue;
257 } else {
258 // Trivially reject a request for a different USAGE
Justin Pauporeb11a8072019-03-12 20:03:25 -0700259 Log.e(TAG, "Client " + entry.getClientId() + " has already requested focus "
260 + "for " + entry.mAfi.getAttributes().usageToString() + " - cannot "
261 + "request focus for " + afi.getAttributes().usageToString() + " on "
262 + "same listener.");
Scott Randolph8eb49552018-10-17 00:56:39 -0700263 return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
264 }
265 }
266
Scott Randolph54892f82018-03-02 13:24:55 -0800267 // Check the interaction matrix for the relationship between this entry and the request
268 switch (sInteractionMatrix[entry.mAudioContext][requestedContext]) {
269 case INTERACTION_REJECT:
270 // Even though this entry has currently lost focus, the fact that it is
271 // waiting to play means we'll reject this new conflicting request.
272 return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
273 case INTERACTION_EXCLUSIVE:
274 // The new request is yet another reason this entry cannot regain focus (yet)
275 blocked.add(entry);
276 break;
277 default:
278 // If ducking is not allowed by the requester, or the pending focus holder had
279 // set the AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS flag,
280 // then the pending holder must stay "lost" until this requester goes away.
Justin Pauporeb11a8072019-03-12 20:03:25 -0700281 if (!allowDucking || entry.wantsPauseInsteadOfDucking()) {
Scott Randolph54892f82018-03-02 13:24:55 -0800282 // The new request is yet another reason this entry cannot regain focus yet
283 blocked.add(entry);
284 }
285 }
286 }
287
288
289 // Now that we've decided we'll grant focus, construct our new FocusEntry
290 FocusEntry newEntry = new FocusEntry(afi, requestedContext);
291
Justin Pauporeb11a8072019-03-12 20:03:25 -0700292 // These entries have permanently lost focus as a result of this request, so they
293 // should be removed from all blocker lists.
294 ArrayList<FocusEntry> permanentlyLost = new ArrayList<>();
295
296 if (replacedCurrentEntry != null) {
297 mFocusHolders.remove(replacedCurrentEntry.getClientId());
298 permanentlyLost.add(replacedCurrentEntry);
299 }
300 if (replacedBlockedEntry != null) {
301 mFocusLosers.remove(replacedBlockedEntry.getClientId());
302 permanentlyLost.add(replacedBlockedEntry);
303 }
304
Scott Randolph54892f82018-03-02 13:24:55 -0800305
306 // Now that we're sure we'll accept this request, update any requests which we would
307 // block but are already out of focus but waiting to come back
308 for (FocusEntry entry : blocked) {
309 // If we're out of focus it must be because somebody is blocking us
310 assert !entry.mBlockers.isEmpty();
311
312 if (permanent) {
313 // This entry has now lost focus forever
314 sendFocusLoss(entry, permanent);
315 final FocusEntry deadEntry = mFocusLosers.remove(entry.mAfi.getClientId());
316 assert deadEntry != null;
Justin Pauporeb11a8072019-03-12 20:03:25 -0700317 permanentlyLost.add(entry);
Scott Randolph54892f82018-03-02 13:24:55 -0800318 } else {
319 // Note that this new request is yet one more reason we can't (yet) have focus
320 entry.mBlockers.add(newEntry);
321 }
322 }
323
324 // Notify and update any requests which are now losing focus as a result of the new request
325 for (FocusEntry entry : losers) {
326 // If we have focus (but are about to loose it), nobody should be blocking us yet
327 assert entry.mBlockers.isEmpty();
328
329 sendFocusLoss(entry, permanent);
330
331 // The entry no longer holds focus, so take it out of the holders list
332 mFocusHolders.remove(entry.mAfi.getClientId());
333
Justin Pauporeb11a8072019-03-12 20:03:25 -0700334 if (permanent) {
335 permanentlyLost.add(entry);
336 } else {
Scott Randolph54892f82018-03-02 13:24:55 -0800337 // Add ourselves to the list of requests waiting to get focus back and
338 // note why we lost focus so we can tell when it's time to get it back
339 mFocusLosers.put(entry.mAfi.getClientId(), entry);
340 entry.mBlockers.add(newEntry);
341 }
342 }
343
Justin Pauporeb11a8072019-03-12 20:03:25 -0700344 // Now that all new blockers have been added, clear out any other requests that have been
345 // permanently lost as a result of this request. Treat them as abandoned - if they're on
346 // any blocker lists, remove them. If any focus requests become unblocked as a result,
347 // re-grant them. (This can happen when a GAIN_TRANSIENT_MAY_DUCK request replaces a
348 // GAIN_TRANSIENT request from the same listener.)
349 for (FocusEntry entry : permanentlyLost) {
350 Log.d(TAG, "Cleaning up entry " + entry.getClientId());
351 removeFocusEntryAndRestoreUnblockedWaiters(entry);
Scott Randolph8eb49552018-10-17 00:56:39 -0700352 }
353
Scott Randolph54892f82018-03-02 13:24:55 -0800354 // Finally, add the request we're granting to the focus holders' list
355 mFocusHolders.put(afi.getClientId(), newEntry);
356
Scott Randolph8eb49552018-10-17 00:56:39 -0700357 Log.i(TAG, "AUDIOFOCUS_REQUEST_GRANTED");
Scott Randolph54892f82018-03-02 13:24:55 -0800358 return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
359 }
360
361
362 @Override
363 public synchronized void onAudioFocusRequest(AudioFocusInfo afi, int requestResult) {
Justin Pauporeb11a8072019-03-12 20:03:25 -0700364 Log.i(TAG, "onAudioFocusRequest " + afi.getClientId());
Scott Randolph54892f82018-03-02 13:24:55 -0800365
366 int response = evaluateFocusRequest(afi);
367
368 // Post our reply for delivery to the original focus requester
369 mAudioManager.setFocusRequestResult(afi, response, mAudioPolicy);
370 }
371
372
373 /**
374 * @see AudioManager#abandonAudioFocus(AudioManager.OnAudioFocusChangeListener, AudioAttributes)
Roberto Perez0a15c812018-10-31 12:53:38 -0700375 * Note that we'll get this call for a focus holder that dies while in the focus stack, so
Scott Randolph54892f82018-03-02 13:24:55 -0800376 * we don't need to watch for death notifications directly.
377 * */
378 @Override
379 public synchronized void onAudioFocusAbandon(AudioFocusInfo afi) {
Justin Pauporeb11a8072019-03-12 20:03:25 -0700380 Log.i(TAG, "onAudioFocusAbandon " + afi.getClientId());
Scott Randolph54892f82018-03-02 13:24:55 -0800381
382 // Remove this entry from our active or pending list
383 FocusEntry deadEntry = mFocusHolders.remove(afi.getClientId());
384 if (deadEntry == null) {
385 deadEntry = mFocusLosers.remove(afi.getClientId());
386 if (deadEntry == null) {
387 // Caller is providing an unrecognzied clientId!?
Scott Randolphdc410612018-10-18 18:43:26 -0700388 Log.w(TAG, "Audio focus abandoned by unrecognized client id: " + afi.getClientId());
389 // This probably means an app double released focused for some reason. One
390 // harmless possibility is a race between an app being told it lost focus and the
391 // app voluntarily abandoning focus. More likely the app is just sloppy. :)
392 // The more nefarious possibility is that the clientId is actually corrupted
393 // somehow, in which case we might have a real focus entry that we're going to fail
394 // to remove. If that were to happen, I'd expect either the app to swallow it
395 // silently, or else take unexpected action (eg: resume playing spontaneously), or
396 // else to see "Failure to signal ..." gain/loss error messages in the log from
397 // this module when a focus change tries to take action on a truly zombie entry.
Roberto Perez0a15c812018-10-31 12:53:38 -0700398 return;
Scott Randolph54892f82018-03-02 13:24:55 -0800399 }
400 }
401
Justin Pauporeb11a8072019-03-12 20:03:25 -0700402 removeFocusEntryAndRestoreUnblockedWaiters(deadEntry);
403 }
404
405 private void removeFocusEntryAndRestoreUnblockedWaiters(FocusEntry deadEntry) {
Scott Randolph54892f82018-03-02 13:24:55 -0800406 // Remove this entry from the blocking list of any pending requests
407 Iterator<FocusEntry> it = mFocusLosers.values().iterator();
408 while (it.hasNext()) {
409 FocusEntry entry = it.next();
410
411 // Remove the retiring entry from all blocker lists
412 entry.mBlockers.remove(deadEntry);
413
414 // Any entry whose blocking list becomes empty should regain focus
415 if (entry.mBlockers.isEmpty()) {
Justin Pauporeb11a8072019-03-12 20:03:25 -0700416 Log.i(TAG, "Restoring unblocked entry " + entry.getClientId());
Scott Randolph54892f82018-03-02 13:24:55 -0800417 // Pull this entry out of the focus losers list
418 it.remove();
419
420 // Add it back into the focus holders list
421 mFocusHolders.put(entry.getClientId(), entry);
422
423 // Send the focus (re)gain notification
424 int result = mAudioManager.dispatchAudioFocusChange(
425 entry.mAfi,
426 entry.mAfi.getGainRequest(),
427 mAudioPolicy);
428 if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
429 // TODO: Is this actually an error, or is it okay for an entry in the focus
430 // stack to NOT have a listener? If that's the case, should we even keep
431 // it in the focus stack?
432 Log.e(TAG, "Failure to signal gain of audio focus with error: " + result);
433 }
434 }
435 }
436 }
437
Scott Randolph54892f82018-03-02 13:24:55 -0800438 public synchronized void dump(PrintWriter writer) {
439 writer.println("*CarAudioFocus*");
440
441 writer.println(" Current Focus Holders:");
442 for (String clientId : mFocusHolders.keySet()) {
443 System.out.println(clientId);
444 }
445
446 writer.println(" Transient Focus Losers:");
447 for (String clientId : mFocusLosers.keySet()) {
448 System.out.println(clientId);
449 }
450 }
451}