1 /* 2 * Copyright (C) 2013 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 */ 16 17 package com.android.server.audio; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.AppOpsManager; 22 import android.content.Context; 23 import android.media.AudioAttributes; 24 import android.media.AudioFocusInfo; 25 import android.media.AudioManager; 26 import android.media.AudioSystem; 27 import android.media.IAudioFocusDispatcher; 28 import android.media.MediaMetrics; 29 import android.media.audiopolicy.IAudioPolicyCallback; 30 import android.os.Binder; 31 import android.os.Build; 32 import android.os.IBinder; 33 import android.os.RemoteException; 34 import android.provider.Settings; 35 import android.util.Log; 36 37 import com.android.internal.annotations.GuardedBy; 38 39 import java.io.PrintWriter; 40 import java.text.DateFormat; 41 import java.util.ArrayList; 42 import java.util.Date; 43 import java.util.HashMap; 44 import java.util.Iterator; 45 import java.util.LinkedList; 46 import java.util.List; 47 import java.util.Map.Entry; 48 import java.util.Set; 49 import java.util.Stack; 50 51 /** 52 * @hide 53 * 54 */ 55 public class MediaFocusControl implements PlayerFocusEnforcer { 56 57 private static final String TAG = "MediaFocusControl"; 58 static final boolean DEBUG = false; 59 60 /** 61 * set to true so the framework enforces ducking itself, without communicating to apps 62 * that they lost focus for most use cases. 63 */ 64 static final boolean ENFORCE_DUCKING = true; 65 /** 66 * set to true to the framework enforces ducking itself only with apps above a given SDK 67 * target level. Is ignored if ENFORCE_DUCKING is false. 68 */ 69 static final boolean ENFORCE_DUCKING_FOR_NEW = true; 70 /** 71 * the SDK level (included) up to which the framework doesn't enforce ducking itself. Is ignored 72 * if ENFORCE_DUCKING_FOR_NEW is false; 73 */ 74 // automatic ducking was introduced for Android O 75 static final int DUCKING_IN_APP_SDK_LEVEL = Build.VERSION_CODES.N_MR1; 76 /** 77 * set to true so the framework enforces muting media/game itself when the device is ringing 78 * or in a call. 79 */ 80 static final boolean ENFORCE_MUTING_FOR_RING_OR_CALL = true; 81 82 private final Context mContext; 83 private final AppOpsManager mAppOps; 84 private PlayerFocusEnforcer mFocusEnforcer; // never null 85 private boolean mMultiAudioFocusEnabled = false; 86 87 private boolean mRingOrCallActive = false; 88 89 private final Object mExtFocusChangeLock = new Object(); 90 @GuardedBy("mExtFocusChangeLock") 91 private long mExtFocusChangeCounter; 92 MediaFocusControl(Context cntxt, PlayerFocusEnforcer pfe)93 protected MediaFocusControl(Context cntxt, PlayerFocusEnforcer pfe) { 94 mContext = cntxt; 95 mAppOps = (AppOpsManager)mContext.getSystemService(Context.APP_OPS_SERVICE); 96 mFocusEnforcer = pfe; 97 mMultiAudioFocusEnabled = Settings.System.getInt(mContext.getContentResolver(), 98 Settings.System.MULTI_AUDIO_FOCUS_ENABLED, 0) != 0; 99 } 100 dump(PrintWriter pw)101 protected void dump(PrintWriter pw) { 102 pw.println("\nMediaFocusControl dump time: " 103 + DateFormat.getTimeInstance().format(new Date())); 104 dumpFocusStack(pw); 105 pw.println("\n"); 106 // log 107 mEventLogger.dump(pw); 108 dumpMultiAudioFocus(pw); 109 } 110 111 //================================================================= 112 // PlayerFocusEnforcer implementation 113 @Override duckPlayers(@onNull FocusRequester winner, @NonNull FocusRequester loser, boolean forceDuck)114 public boolean duckPlayers(@NonNull FocusRequester winner, @NonNull FocusRequester loser, 115 boolean forceDuck) { 116 return mFocusEnforcer.duckPlayers(winner, loser, forceDuck); 117 } 118 119 @Override unduckPlayers(@onNull FocusRequester winner)120 public void unduckPlayers(@NonNull FocusRequester winner) { 121 mFocusEnforcer.unduckPlayers(winner); 122 } 123 124 @Override mutePlayersForCall(int[] usagesToMute)125 public void mutePlayersForCall(int[] usagesToMute) { 126 mFocusEnforcer.mutePlayersForCall(usagesToMute); 127 } 128 129 @Override unmutePlayersForCall()130 public void unmutePlayersForCall() { 131 mFocusEnforcer.unmutePlayersForCall(); 132 } 133 134 //========================================================================================== 135 // AudioFocus 136 //========================================================================================== 137 138 private final static Object mAudioFocusLock = new Object(); 139 140 /** 141 * Arbitrary maximum size of audio focus stack to prevent apps OOM'ing this process. 142 */ 143 private static final int MAX_STACK_SIZE = 100; 144 145 private static final AudioEventLogger mEventLogger = new AudioEventLogger(50, 146 "focus commands as seen by MediaFocusControl"); 147 148 private static final String mMetricsId = MediaMetrics.Name.AUDIO_FOCUS; 149 noFocusForSuspendedApp(@onNull String packageName, int uid)150 /*package*/ void noFocusForSuspendedApp(@NonNull String packageName, int uid) { 151 synchronized (mAudioFocusLock) { 152 final Iterator<FocusRequester> stackIterator = mFocusStack.iterator(); 153 List<String> clientsToRemove = new ArrayList<>(); 154 while (stackIterator.hasNext()) { 155 final FocusRequester focusOwner = stackIterator.next(); 156 if (focusOwner.hasSameUid(uid) && focusOwner.hasSamePackage(packageName)) { 157 clientsToRemove.add(focusOwner.getClientId()); 158 mEventLogger.log((new AudioEventLogger.StringEvent( 159 "focus owner:" + focusOwner.getClientId() 160 + " in uid:" + uid + " pack: " + packageName 161 + " getting AUDIOFOCUS_LOSS due to app suspension")) 162 .printLog(TAG)); 163 // make the suspended app lose focus through its focus listener (if any) 164 focusOwner.dispatchFocusChange(AudioManager.AUDIOFOCUS_LOSS); 165 } 166 } 167 for (String clientToRemove : clientsToRemove) { 168 // update the stack but don't signal the change. 169 removeFocusStackEntry(clientToRemove, false, true); 170 } 171 } 172 } 173 hasAudioFocusUsers()174 /*package*/ boolean hasAudioFocusUsers() { 175 synchronized (mAudioFocusLock) { 176 return !mFocusStack.empty(); 177 } 178 } 179 180 /** 181 * Discard the current audio focus owner. 182 * Notify top of audio focus stack that it lost focus (regardless of possibility to reassign 183 * focus), remove it from the stack, and clear the remote control display. 184 */ discardAudioFocusOwner()185 protected void discardAudioFocusOwner() { 186 synchronized(mAudioFocusLock) { 187 if (!mFocusStack.empty()) { 188 // notify the current focus owner it lost focus after removing it from stack 189 final FocusRequester exFocusOwner = mFocusStack.pop(); 190 exFocusOwner.handleFocusLoss(AudioManager.AUDIOFOCUS_LOSS, null, 191 false /*forceDuck*/); 192 exFocusOwner.release(); 193 } 194 } 195 } 196 197 @GuardedBy("mAudioFocusLock") notifyTopOfAudioFocusStack()198 private void notifyTopOfAudioFocusStack() { 199 // notify the top of the stack it gained focus 200 if (!mFocusStack.empty()) { 201 if (canReassignAudioFocus()) { 202 mFocusStack.peek().handleFocusGain(AudioManager.AUDIOFOCUS_GAIN); 203 } 204 } 205 206 if (mMultiAudioFocusEnabled && !mMultiAudioFocusList.isEmpty()) { 207 for (FocusRequester multifr : mMultiAudioFocusList) { 208 if (isLockedFocusOwner(multifr)) { 209 multifr.handleFocusGain(AudioManager.AUDIOFOCUS_GAIN); 210 } 211 } 212 } 213 } 214 215 /** 216 * Focus is requested, propagate the associated loss throughout the stack. 217 * Will also remove entries in the stack that have just received a definitive loss of focus. 218 * @param focusGain the new focus gain that will later be added at the top of the stack 219 */ 220 @GuardedBy("mAudioFocusLock") propagateFocusLossFromGain_syncAf(int focusGain, final FocusRequester fr, boolean forceDuck)221 private void propagateFocusLossFromGain_syncAf(int focusGain, final FocusRequester fr, 222 boolean forceDuck) { 223 final List<String> clientsToRemove = new LinkedList<String>(); 224 // going through the audio focus stack to signal new focus, traversing order doesn't 225 // matter as all entries respond to the same external focus gain 226 if (!mFocusStack.empty()) { 227 for (FocusRequester focusLoser : mFocusStack) { 228 final boolean isDefinitiveLoss = 229 focusLoser.handleFocusLossFromGain(focusGain, fr, forceDuck); 230 if (isDefinitiveLoss) { 231 clientsToRemove.add(focusLoser.getClientId()); 232 } 233 } 234 } 235 236 if (mMultiAudioFocusEnabled && !mMultiAudioFocusList.isEmpty()) { 237 for (FocusRequester multifocusLoser : mMultiAudioFocusList) { 238 final boolean isDefinitiveLoss = 239 multifocusLoser.handleFocusLossFromGain(focusGain, fr, forceDuck); 240 if (isDefinitiveLoss) { 241 clientsToRemove.add(multifocusLoser.getClientId()); 242 } 243 } 244 } 245 246 for (String clientToRemove : clientsToRemove) { 247 removeFocusStackEntry(clientToRemove, false /*signal*/, 248 true /*notifyFocusFollowers*/); 249 } 250 } 251 252 private final Stack<FocusRequester> mFocusStack = new Stack<FocusRequester>(); 253 254 ArrayList<FocusRequester> mMultiAudioFocusList = new ArrayList<FocusRequester>(); 255 256 /** 257 * Helper function: 258 * Display in the log the current entries in the audio focus stack 259 */ dumpFocusStack(PrintWriter pw)260 private void dumpFocusStack(PrintWriter pw) { 261 pw.println("\nAudio Focus stack entries (last is top of stack):"); 262 synchronized(mAudioFocusLock) { 263 Iterator<FocusRequester> stackIterator = mFocusStack.iterator(); 264 while(stackIterator.hasNext()) { 265 stackIterator.next().dump(pw); 266 } 267 pw.println("\n"); 268 if (mFocusPolicy == null) { 269 pw.println("No external focus policy\n"); 270 } else { 271 pw.println("External focus policy: "+ mFocusPolicy + ", focus owners:\n"); 272 dumpExtFocusPolicyFocusOwners(pw); 273 } 274 } 275 pw.println("\n"); 276 pw.println(" Notify on duck: " + mNotifyFocusOwnerOnDuck + "\n"); 277 pw.println(" In ring or call: " + mRingOrCallActive + "\n"); 278 } 279 280 /** 281 * Remove a focus listener from the focus stack. 282 * @param clientToRemove the focus listener 283 * @param signal if true and the listener was at the top of the focus stack, i.e. it was holding 284 * focus, notify the next item in the stack it gained focus. 285 */ 286 @GuardedBy("mAudioFocusLock") removeFocusStackEntry(String clientToRemove, boolean signal, boolean notifyFocusFollowers)287 private void removeFocusStackEntry(String clientToRemove, boolean signal, 288 boolean notifyFocusFollowers) { 289 // is the current top of the focus stack abandoning focus? (because of request, not death) 290 if (!mFocusStack.empty() && mFocusStack.peek().hasSameClient(clientToRemove)) 291 { 292 //Log.i(TAG, " removeFocusStackEntry() removing top of stack"); 293 FocusRequester fr = mFocusStack.pop(); 294 fr.release(); 295 if (notifyFocusFollowers) { 296 final AudioFocusInfo afi = fr.toAudioFocusInfo(); 297 afi.clearLossReceived(); 298 notifyExtPolicyFocusLoss_syncAf(afi, false); 299 } 300 if (signal) { 301 // notify the new top of the stack it gained focus 302 notifyTopOfAudioFocusStack(); 303 } 304 } else { 305 // focus is abandoned by a client that's not at the top of the stack, 306 // no need to update focus. 307 // (using an iterator on the stack so we can safely remove an entry after having 308 // evaluated it, traversal order doesn't matter here) 309 Iterator<FocusRequester> stackIterator = mFocusStack.iterator(); 310 while(stackIterator.hasNext()) { 311 FocusRequester fr = stackIterator.next(); 312 if(fr.hasSameClient(clientToRemove)) { 313 Log.i(TAG, "AudioFocus removeFocusStackEntry(): removing entry for " 314 + clientToRemove); 315 stackIterator.remove(); 316 // stack entry not used anymore, clear references 317 fr.release(); 318 } 319 } 320 } 321 322 if (mMultiAudioFocusEnabled && !mMultiAudioFocusList.isEmpty()) { 323 Iterator<FocusRequester> listIterator = mMultiAudioFocusList.iterator(); 324 while (listIterator.hasNext()) { 325 FocusRequester fr = listIterator.next(); 326 if (fr.hasSameClient(clientToRemove)) { 327 listIterator.remove(); 328 fr.release(); 329 } 330 } 331 332 if (signal) { 333 // notify the new top of the stack it gained focus 334 notifyTopOfAudioFocusStack(); 335 } 336 } 337 } 338 339 /** 340 * Remove focus listeners from the focus stack for a particular client when it has died. 341 */ 342 @GuardedBy("mAudioFocusLock") removeFocusStackEntryOnDeath(IBinder cb)343 private void removeFocusStackEntryOnDeath(IBinder cb) { 344 // is the owner of the audio focus part of the client to remove? 345 boolean isTopOfStackForClientToRemove = !mFocusStack.isEmpty() && 346 mFocusStack.peek().hasSameBinder(cb); 347 // (using an iterator on the stack so we can safely remove an entry after having 348 // evaluated it, traversal order doesn't matter here) 349 Iterator<FocusRequester> stackIterator = mFocusStack.iterator(); 350 while(stackIterator.hasNext()) { 351 FocusRequester fr = stackIterator.next(); 352 if(fr.hasSameBinder(cb)) { 353 Log.i(TAG, "AudioFocus removeFocusStackEntryOnDeath(): removing entry for " + cb); 354 stackIterator.remove(); 355 // stack entry not used anymore, clear references 356 fr.release(); 357 } 358 } 359 if (isTopOfStackForClientToRemove) { 360 // we removed an entry at the top of the stack: 361 // notify the new top of the stack it gained focus. 362 notifyTopOfAudioFocusStack(); 363 } 364 } 365 366 /** 367 * Helper function for external focus policy: 368 * Remove focus listeners from the list of potential focus owners for a particular client when 369 * it has died. 370 */ 371 @GuardedBy("mAudioFocusLock") removeFocusEntryForExtPolicy(IBinder cb)372 private void removeFocusEntryForExtPolicy(IBinder cb) { 373 if (mFocusOwnersForFocusPolicy.isEmpty()) { 374 return; 375 } 376 boolean released = false; 377 final Set<Entry<String, FocusRequester>> owners = mFocusOwnersForFocusPolicy.entrySet(); 378 final Iterator<Entry<String, FocusRequester>> ownerIterator = owners.iterator(); 379 while (ownerIterator.hasNext()) { 380 final Entry<String, FocusRequester> owner = ownerIterator.next(); 381 final FocusRequester fr = owner.getValue(); 382 if (fr.hasSameBinder(cb)) { 383 ownerIterator.remove(); 384 fr.release(); 385 notifyExtFocusPolicyFocusAbandon_syncAf(fr.toAudioFocusInfo()); 386 break; 387 } 388 } 389 } 390 391 /** 392 * Helper function: 393 * Returns true if the system is in a state where the focus can be reevaluated, false otherwise. 394 * The implementation guarantees that a state where focus cannot be immediately reassigned 395 * implies that an "locked" focus owner is at the top of the focus stack. 396 * Modifications to the implementation that break this assumption will cause focus requests to 397 * misbehave when honoring the AudioManager.AUDIOFOCUS_FLAG_DELAY_OK flag. 398 */ canReassignAudioFocus()399 private boolean canReassignAudioFocus() { 400 // focus requests are rejected during a phone call or when the phone is ringing 401 // this is equivalent to IN_VOICE_COMM_FOCUS_ID having the focus 402 if (!mFocusStack.isEmpty() && isLockedFocusOwner(mFocusStack.peek())) { 403 return false; 404 } 405 return true; 406 } 407 isLockedFocusOwner(FocusRequester fr)408 private boolean isLockedFocusOwner(FocusRequester fr) { 409 return (fr.hasSameClient(AudioSystem.IN_VOICE_COMM_FOCUS_ID) || fr.isLockedFocusOwner()); 410 } 411 412 /** 413 * Helper function 414 * Pre-conditions: focus stack is not empty, there is one or more locked focus owner 415 * at the top of the focus stack 416 * Push the focus requester onto the audio focus stack at the first position immediately 417 * following the locked focus owners. 418 * @return {@link AudioManager#AUDIOFOCUS_REQUEST_GRANTED} or 419 * {@link AudioManager#AUDIOFOCUS_REQUEST_DELAYED} 420 */ 421 @GuardedBy("mAudioFocusLock") pushBelowLockedFocusOwners(FocusRequester nfr)422 private int pushBelowLockedFocusOwners(FocusRequester nfr) { 423 int lastLockedFocusOwnerIndex = mFocusStack.size(); 424 for (int index = mFocusStack.size()-1; index >= 0; index--) { 425 if (isLockedFocusOwner(mFocusStack.elementAt(index))) { 426 lastLockedFocusOwnerIndex = index; 427 } 428 } 429 if (lastLockedFocusOwnerIndex == mFocusStack.size()) { 430 // this should not happen, but handle it and log an error 431 Log.e(TAG, "No exclusive focus owner found in propagateFocusLossFromGain_syncAf()", 432 new Exception()); 433 // no exclusive owner, push at top of stack, focus is granted, propagate change 434 propagateFocusLossFromGain_syncAf(nfr.getGainRequest(), nfr, false /*forceDuck*/); 435 mFocusStack.push(nfr); 436 return AudioManager.AUDIOFOCUS_REQUEST_GRANTED; 437 } else { 438 mFocusStack.insertElementAt(nfr, lastLockedFocusOwnerIndex); 439 return AudioManager.AUDIOFOCUS_REQUEST_DELAYED; 440 } 441 } 442 443 /** 444 * Inner class to monitor audio focus client deaths, and remove them from the audio focus 445 * stack if necessary. 446 */ 447 protected class AudioFocusDeathHandler implements IBinder.DeathRecipient { 448 private IBinder mCb; // To be notified of client's death 449 AudioFocusDeathHandler(IBinder cb)450 AudioFocusDeathHandler(IBinder cb) { 451 mCb = cb; 452 } 453 binderDied()454 public void binderDied() { 455 synchronized(mAudioFocusLock) { 456 if (mFocusPolicy != null) { 457 removeFocusEntryForExtPolicy(mCb); 458 } else { 459 removeFocusStackEntryOnDeath(mCb); 460 if (mMultiAudioFocusEnabled && !mMultiAudioFocusList.isEmpty()) { 461 Iterator<FocusRequester> listIterator = mMultiAudioFocusList.iterator(); 462 while (listIterator.hasNext()) { 463 FocusRequester fr = listIterator.next(); 464 if (fr.hasSameBinder(mCb)) { 465 listIterator.remove(); 466 fr.release(); 467 } 468 } 469 } 470 } 471 } 472 } 473 } 474 475 /** 476 * Indicates whether to notify an audio focus owner when it loses focus 477 * with {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK} if it will only duck. 478 * This variable being false indicates an AudioPolicy has been registered and has signaled 479 * it will handle audio ducking. 480 */ 481 private boolean mNotifyFocusOwnerOnDuck = true; 482 setDuckingInExtPolicyAvailable(boolean available)483 protected void setDuckingInExtPolicyAvailable(boolean available) { 484 mNotifyFocusOwnerOnDuck = !available; 485 } 486 mustNotifyFocusOwnerOnDuck()487 boolean mustNotifyFocusOwnerOnDuck() { return mNotifyFocusOwnerOnDuck; } 488 489 private ArrayList<IAudioPolicyCallback> mFocusFollowers = new ArrayList<IAudioPolicyCallback>(); 490 addFocusFollower(IAudioPolicyCallback ff)491 void addFocusFollower(IAudioPolicyCallback ff) { 492 if (ff == null) { 493 return; 494 } 495 synchronized(mAudioFocusLock) { 496 boolean found = false; 497 for (IAudioPolicyCallback pcb : mFocusFollowers) { 498 if (pcb.asBinder().equals(ff.asBinder())) { 499 found = true; 500 break; 501 } 502 } 503 if (found) { 504 return; 505 } else { 506 mFocusFollowers.add(ff); 507 notifyExtPolicyCurrentFocusAsync(ff); 508 } 509 } 510 } 511 removeFocusFollower(IAudioPolicyCallback ff)512 void removeFocusFollower(IAudioPolicyCallback ff) { 513 if (ff == null) { 514 return; 515 } 516 synchronized(mAudioFocusLock) { 517 for (IAudioPolicyCallback pcb : mFocusFollowers) { 518 if (pcb.asBinder().equals(ff.asBinder())) { 519 mFocusFollowers.remove(pcb); 520 break; 521 } 522 } 523 } 524 } 525 526 /** The current audio focus policy */ 527 @GuardedBy("mAudioFocusLock") 528 @Nullable private IAudioPolicyCallback mFocusPolicy = null; 529 /** 530 * The audio focus policy that was registered before a test focus policy was registered 531 * during a test 532 */ 533 @GuardedBy("mAudioFocusLock") 534 @Nullable private IAudioPolicyCallback mPreviousFocusPolicy = null; 535 536 // Since we don't have a stack of focus owners when using an external focus policy, we keep 537 // track of all the focus requesters in this map, with their clientId as the key. This is 538 // used both for focus dispatch and death handling 539 private HashMap<String, FocusRequester> mFocusOwnersForFocusPolicy = 540 new HashMap<String, FocusRequester>(); 541 setFocusPolicy(IAudioPolicyCallback policy, boolean isTestFocusPolicy)542 void setFocusPolicy(IAudioPolicyCallback policy, boolean isTestFocusPolicy) { 543 if (policy == null) { 544 return; 545 } 546 synchronized (mAudioFocusLock) { 547 if (isTestFocusPolicy) { 548 mPreviousFocusPolicy = mFocusPolicy; 549 } 550 mFocusPolicy = policy; 551 } 552 } 553 unsetFocusPolicy(IAudioPolicyCallback policy, boolean isTestFocusPolicy)554 void unsetFocusPolicy(IAudioPolicyCallback policy, boolean isTestFocusPolicy) { 555 if (policy == null) { 556 return; 557 } 558 synchronized (mAudioFocusLock) { 559 if (mFocusPolicy == policy) { 560 if (isTestFocusPolicy) { 561 // restore the focus policy that was there before the focus policy test started 562 mFocusPolicy = mPreviousFocusPolicy; 563 } else { 564 mFocusPolicy = null; 565 } 566 } 567 } 568 } 569 570 /** 571 * @param pcb non null 572 */ notifyExtPolicyCurrentFocusAsync(IAudioPolicyCallback pcb)573 void notifyExtPolicyCurrentFocusAsync(IAudioPolicyCallback pcb) { 574 final IAudioPolicyCallback pcb2 = pcb; 575 final Thread thread = new Thread() { 576 @Override 577 public void run() { 578 synchronized(mAudioFocusLock) { 579 if (mFocusStack.isEmpty()) { 580 return; 581 } 582 try { 583 pcb2.notifyAudioFocusGrant(mFocusStack.peek().toAudioFocusInfo(), 584 // top of focus stack always has focus 585 AudioManager.AUDIOFOCUS_REQUEST_GRANTED); 586 } catch (RemoteException e) { 587 Log.e(TAG, "Can't call notifyAudioFocusGrant() on IAudioPolicyCallback " 588 + pcb2.asBinder(), e); 589 } 590 } 591 } 592 }; 593 thread.start(); 594 } 595 596 /** 597 * Called synchronized on mAudioFocusLock 598 */ notifyExtPolicyFocusGrant_syncAf(AudioFocusInfo afi, int requestResult)599 void notifyExtPolicyFocusGrant_syncAf(AudioFocusInfo afi, int requestResult) { 600 for (IAudioPolicyCallback pcb : mFocusFollowers) { 601 try { 602 // oneway 603 pcb.notifyAudioFocusGrant(afi, requestResult); 604 } catch (RemoteException e) { 605 Log.e(TAG, "Can't call notifyAudioFocusGrant() on IAudioPolicyCallback " 606 + pcb.asBinder(), e); 607 } 608 } 609 } 610 611 /** 612 * Called synchronized on mAudioFocusLock 613 */ notifyExtPolicyFocusLoss_syncAf(AudioFocusInfo afi, boolean wasDispatched)614 void notifyExtPolicyFocusLoss_syncAf(AudioFocusInfo afi, boolean wasDispatched) { 615 for (IAudioPolicyCallback pcb : mFocusFollowers) { 616 try { 617 // oneway 618 pcb.notifyAudioFocusLoss(afi, wasDispatched); 619 } catch (RemoteException e) { 620 Log.e(TAG, "Can't call notifyAudioFocusLoss() on IAudioPolicyCallback " 621 + pcb.asBinder(), e); 622 } 623 } 624 } 625 626 /** 627 * Called synchronized on mAudioFocusLock. 628 * Can only be called with an external focus policy installed (mFocusPolicy != null) 629 * @param afi 630 * @param fd 631 * @param cb binder of the focus requester 632 * @return true if the external audio focus policy (if any) can handle the focus request, 633 * and false if there was any error handling the request (e.g. error talking to policy, 634 * focus requester is already dead) 635 */ notifyExtFocusPolicyFocusRequest_syncAf(AudioFocusInfo afi, IAudioFocusDispatcher fd, @NonNull IBinder cb)636 boolean notifyExtFocusPolicyFocusRequest_syncAf(AudioFocusInfo afi, 637 IAudioFocusDispatcher fd, @NonNull IBinder cb) { 638 if (DEBUG) { 639 Log.v(TAG, "notifyExtFocusPolicyFocusRequest client="+afi.getClientId() 640 + " dispatcher=" + fd); 641 } 642 synchronized (mExtFocusChangeLock) { 643 afi.setGen(mExtFocusChangeCounter++); 644 } 645 final FocusRequester existingFr = mFocusOwnersForFocusPolicy.get(afi.getClientId()); 646 boolean keepTrack = false; 647 if (existingFr != null) { 648 if (!existingFr.hasSameDispatcher(fd)) { 649 existingFr.release(); 650 keepTrack = true; 651 } 652 } else { 653 keepTrack = true; 654 } 655 if (keepTrack) { 656 final AudioFocusDeathHandler hdlr = new AudioFocusDeathHandler(cb); 657 try { 658 cb.linkToDeath(hdlr, 0); 659 } catch (RemoteException e) { 660 // client has already died! 661 return false; 662 } 663 // new focus (future) focus owner to keep track of 664 mFocusOwnersForFocusPolicy.put(afi.getClientId(), 665 new FocusRequester(afi, fd, cb, hdlr, this)); 666 } 667 668 try { 669 //oneway 670 mFocusPolicy.notifyAudioFocusRequest(afi, AudioManager.AUDIOFOCUS_REQUEST_GRANTED); 671 return true; 672 } catch (RemoteException e) { 673 Log.e(TAG, "Can't call notifyAudioFocusRequest() on IAudioPolicyCallback " 674 + mFocusPolicy.asBinder(), e); 675 } 676 return false; 677 } 678 setFocusRequestResultFromExtPolicy(AudioFocusInfo afi, int requestResult)679 void setFocusRequestResultFromExtPolicy(AudioFocusInfo afi, int requestResult) { 680 synchronized (mExtFocusChangeLock) { 681 if (afi.getGen() > mExtFocusChangeCounter) { 682 return; 683 } 684 } 685 final FocusRequester fr = mFocusOwnersForFocusPolicy.get(afi.getClientId()); 686 if (fr != null) { 687 fr.dispatchFocusResultFromExtPolicy(requestResult); 688 } 689 } 690 691 /** 692 * Called synchronized on mAudioFocusLock 693 * @param afi 694 * @return true if the external audio focus policy (if any) is handling the focus request 695 */ notifyExtFocusPolicyFocusAbandon_syncAf(AudioFocusInfo afi)696 boolean notifyExtFocusPolicyFocusAbandon_syncAf(AudioFocusInfo afi) { 697 if (mFocusPolicy == null) { 698 return false; 699 } 700 final FocusRequester fr = mFocusOwnersForFocusPolicy.remove(afi.getClientId()); 701 if (fr != null) { 702 fr.release(); 703 } 704 try { 705 //oneway 706 mFocusPolicy.notifyAudioFocusAbandon(afi); 707 } catch (RemoteException e) { 708 Log.e(TAG, "Can't call notifyAudioFocusAbandon() on IAudioPolicyCallback " 709 + mFocusPolicy.asBinder(), e); 710 } 711 return true; 712 } 713 714 /** see AudioManager.dispatchFocusChange(AudioFocusInfo afi, int focusChange, AudioPolicy ap) */ dispatchFocusChange(AudioFocusInfo afi, int focusChange)715 int dispatchFocusChange(AudioFocusInfo afi, int focusChange) { 716 if (DEBUG) { 717 Log.v(TAG, "dispatchFocusChange " + focusChange + " to afi client=" 718 + afi.getClientId()); 719 } 720 synchronized (mAudioFocusLock) { 721 if (mFocusPolicy == null) { 722 if (DEBUG) { Log.v(TAG, "> failed: no focus policy" ); } 723 return AudioManager.AUDIOFOCUS_REQUEST_FAILED; 724 } 725 final FocusRequester fr; 726 if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { 727 fr = mFocusOwnersForFocusPolicy.remove(afi.getClientId()); 728 } else { 729 fr = mFocusOwnersForFocusPolicy.get(afi.getClientId()); 730 } 731 if (fr == null) { 732 if (DEBUG) { Log.v(TAG, "> failed: no such focus requester known" ); } 733 return AudioManager.AUDIOFOCUS_REQUEST_FAILED; 734 } 735 return fr.dispatchFocusChange(focusChange); 736 } 737 } 738 dumpExtFocusPolicyFocusOwners(PrintWriter pw)739 private void dumpExtFocusPolicyFocusOwners(PrintWriter pw) { 740 final Set<Entry<String, FocusRequester>> owners = mFocusOwnersForFocusPolicy.entrySet(); 741 final Iterator<Entry<String, FocusRequester>> ownerIterator = owners.iterator(); 742 while (ownerIterator.hasNext()) { 743 final Entry<String, FocusRequester> owner = ownerIterator.next(); 744 final FocusRequester fr = owner.getValue(); 745 fr.dump(pw); 746 } 747 } 748 getCurrentAudioFocus()749 protected int getCurrentAudioFocus() { 750 synchronized(mAudioFocusLock) { 751 if (mFocusStack.empty()) { 752 return AudioManager.AUDIOFOCUS_NONE; 753 } else { 754 return mFocusStack.peek().getGainRequest(); 755 } 756 } 757 } 758 759 /** 760 * Delay after entering ringing or call mode after which the framework will mute streams 761 * that are still playing. 762 */ 763 private static final int RING_CALL_MUTING_ENFORCEMENT_DELAY_MS = 100; 764 765 /** 766 * Usages to mute when the device rings or is in a call 767 */ 768 private final static int[] USAGES_TO_MUTE_IN_RING_OR_CALL = 769 { AudioAttributes.USAGE_MEDIA, AudioAttributes.USAGE_GAME }; 770 771 /** 772 * Return the volume ramp time expected before playback with the given AudioAttributes would 773 * start after gaining audio focus. 774 * @param attr attributes of the sound about to start playing 775 * @return time in ms 776 */ getFocusRampTimeMs(int focusGain, AudioAttributes attr)777 protected static int getFocusRampTimeMs(int focusGain, AudioAttributes attr) { 778 switch (attr.getUsage()) { 779 case AudioAttributes.USAGE_MEDIA: 780 case AudioAttributes.USAGE_GAME: 781 return 1000; 782 case AudioAttributes.USAGE_ALARM: 783 case AudioAttributes.USAGE_NOTIFICATION_RINGTONE: 784 case AudioAttributes.USAGE_ASSISTANT: 785 case AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY: 786 case AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: 787 case AudioAttributes.USAGE_ANNOUNCEMENT: 788 return 700; 789 case AudioAttributes.USAGE_VOICE_COMMUNICATION: 790 case AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING: 791 case AudioAttributes.USAGE_NOTIFICATION: 792 case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST: 793 case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT: 794 case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED: 795 case AudioAttributes.USAGE_NOTIFICATION_EVENT: 796 case AudioAttributes.USAGE_ASSISTANCE_SONIFICATION: 797 case AudioAttributes.USAGE_VEHICLE_STATUS: 798 return 500; 799 case AudioAttributes.USAGE_EMERGENCY: 800 case AudioAttributes.USAGE_SAFETY: 801 case AudioAttributes.USAGE_UNKNOWN: 802 default: 803 return 0; 804 } 805 } 806 807 /** @see AudioManager#requestAudioFocus(AudioManager.OnAudioFocusChangeListener, int, int, int) 808 * @param aa 809 * @param focusChangeHint 810 * @param cb 811 * @param fd 812 * @param clientId 813 * @param callingPackageName 814 * @param flags 815 * @param sdk 816 * @param forceDuck only true if 817 * {@link android.media.AudioFocusRequest.Builder#setFocusGain(int)} was set to true for 818 * accessibility. 819 * @return 820 */ requestAudioFocus(@onNull AudioAttributes aa, int focusChangeHint, IBinder cb, IAudioFocusDispatcher fd, @NonNull String clientId, @NonNull String callingPackageName, int flags, int sdk, boolean forceDuck)821 protected int requestAudioFocus(@NonNull AudioAttributes aa, int focusChangeHint, IBinder cb, 822 IAudioFocusDispatcher fd, @NonNull String clientId, @NonNull String callingPackageName, 823 int flags, int sdk, boolean forceDuck) { 824 new MediaMetrics.Item(mMetricsId) 825 .setUid(Binder.getCallingUid()) 826 .set(MediaMetrics.Property.CALLING_PACKAGE, callingPackageName) 827 .set(MediaMetrics.Property.CLIENT_NAME, clientId) 828 .set(MediaMetrics.Property.EVENT, "requestAudioFocus") 829 .set(MediaMetrics.Property.FLAGS, flags) 830 .set(MediaMetrics.Property.FOCUS_CHANGE_HINT, 831 AudioManager.audioFocusToString(focusChangeHint)) 832 //.set(MediaMetrics.Property.SDK, sdk) 833 .record(); 834 835 mEventLogger.log((new AudioEventLogger.StringEvent( 836 "requestAudioFocus() from uid/pid " + Binder.getCallingUid() 837 + "/" + Binder.getCallingPid() 838 + " clientId=" + clientId + " callingPack=" + callingPackageName 839 + " req=" + focusChangeHint 840 + " flags=0x" + Integer.toHexString(flags) 841 + " sdk=" + sdk)) 842 .printLog(TAG)); 843 // we need a valid binder callback for clients 844 if (!cb.pingBinder()) { 845 Log.e(TAG, " AudioFocus DOA client for requestAudioFocus(), aborting."); 846 return AudioManager.AUDIOFOCUS_REQUEST_FAILED; 847 } 848 849 if (mAppOps.noteOp(AppOpsManager.OP_TAKE_AUDIO_FOCUS, Binder.getCallingUid(), 850 callingPackageName) != AppOpsManager.MODE_ALLOWED) { 851 return AudioManager.AUDIOFOCUS_REQUEST_FAILED; 852 } 853 854 synchronized(mAudioFocusLock) { 855 if (mFocusStack.size() > MAX_STACK_SIZE) { 856 Log.e(TAG, "Max AudioFocus stack size reached, failing requestAudioFocus()"); 857 return AudioManager.AUDIOFOCUS_REQUEST_FAILED; 858 } 859 860 boolean enteringRingOrCall = !mRingOrCallActive 861 & (AudioSystem.IN_VOICE_COMM_FOCUS_ID.compareTo(clientId) == 0); 862 if (enteringRingOrCall) { mRingOrCallActive = true; } 863 864 final AudioFocusInfo afiForExtPolicy; 865 if (mFocusPolicy != null) { 866 // construct AudioFocusInfo as it will be communicated to audio focus policy 867 afiForExtPolicy = new AudioFocusInfo(aa, Binder.getCallingUid(), 868 clientId, callingPackageName, focusChangeHint, 0 /*lossReceived*/, 869 flags, sdk); 870 } else { 871 afiForExtPolicy = null; 872 } 873 874 // handle delayed focus 875 boolean focusGrantDelayed = false; 876 if (!canReassignAudioFocus()) { 877 if ((flags & AudioManager.AUDIOFOCUS_FLAG_DELAY_OK) == 0) { 878 return AudioManager.AUDIOFOCUS_REQUEST_FAILED; 879 } else { 880 // request has AUDIOFOCUS_FLAG_DELAY_OK: focus can't be 881 // granted right now, so the requester will be inserted in the focus stack 882 // to receive focus later 883 focusGrantDelayed = true; 884 } 885 } 886 887 // external focus policy? 888 if (mFocusPolicy != null) { 889 if (notifyExtFocusPolicyFocusRequest_syncAf(afiForExtPolicy, fd, cb)) { 890 // stop handling focus request here as it is handled by external audio 891 // focus policy (return code will be handled in AudioManager) 892 return AudioManager.AUDIOFOCUS_REQUEST_WAITING_FOR_EXT_POLICY; 893 } else { 894 // an error occured, client already dead, bail early 895 return AudioManager.AUDIOFOCUS_REQUEST_FAILED; 896 } 897 } 898 899 // handle the potential premature death of the new holder of the focus 900 // (premature death == death before abandoning focus) 901 // Register for client death notification 902 AudioFocusDeathHandler afdh = new AudioFocusDeathHandler(cb); 903 904 try { 905 cb.linkToDeath(afdh, 0); 906 } catch (RemoteException e) { 907 // client has already died! 908 Log.w(TAG, "AudioFocus requestAudioFocus() could not link to "+cb+" binder death"); 909 return AudioManager.AUDIOFOCUS_REQUEST_FAILED; 910 } 911 912 if (!mFocusStack.empty() && mFocusStack.peek().hasSameClient(clientId)) { 913 // if focus is already owned by this client and the reason for acquiring the focus 914 // hasn't changed, don't do anything 915 final FocusRequester fr = mFocusStack.peek(); 916 if (fr.getGainRequest() == focusChangeHint && fr.getGrantFlags() == flags) { 917 // unlink death handler so it can be gc'ed. 918 // linkToDeath() creates a JNI global reference preventing collection. 919 cb.unlinkToDeath(afdh, 0); 920 notifyExtPolicyFocusGrant_syncAf(fr.toAudioFocusInfo(), 921 AudioManager.AUDIOFOCUS_REQUEST_GRANTED); 922 return AudioManager.AUDIOFOCUS_REQUEST_GRANTED; 923 } 924 // the reason for the audio focus request has changed: remove the current top of 925 // stack and respond as if we had a new focus owner 926 if (!focusGrantDelayed) { 927 mFocusStack.pop(); 928 // the entry that was "popped" is the same that was "peeked" above 929 fr.release(); 930 } 931 } 932 933 // focus requester might already be somewhere below in the stack, remove it 934 removeFocusStackEntry(clientId, false /* signal */, false /*notifyFocusFollowers*/); 935 936 final FocusRequester nfr = new FocusRequester(aa, focusChangeHint, flags, fd, cb, 937 clientId, afdh, callingPackageName, Binder.getCallingUid(), this, sdk); 938 939 if (mMultiAudioFocusEnabled 940 && (focusChangeHint == AudioManager.AUDIOFOCUS_GAIN)) { 941 if (enteringRingOrCall) { 942 if (!mMultiAudioFocusList.isEmpty()) { 943 for (FocusRequester multifr : mMultiAudioFocusList) { 944 multifr.handleFocusLossFromGain(focusChangeHint, nfr, forceDuck); 945 } 946 } 947 } else { 948 boolean needAdd = true; 949 if (!mMultiAudioFocusList.isEmpty()) { 950 for (FocusRequester multifr : mMultiAudioFocusList) { 951 if (multifr.getClientUid() == Binder.getCallingUid()) { 952 needAdd = false; 953 break; 954 } 955 } 956 } 957 if (needAdd) { 958 mMultiAudioFocusList.add(nfr); 959 } 960 nfr.handleFocusGainFromRequest(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); 961 notifyExtPolicyFocusGrant_syncAf(nfr.toAudioFocusInfo(), 962 AudioManager.AUDIOFOCUS_REQUEST_GRANTED); 963 return AudioManager.AUDIOFOCUS_REQUEST_GRANTED; 964 } 965 } 966 967 if (focusGrantDelayed) { 968 // focusGrantDelayed being true implies we can't reassign focus right now 969 // which implies the focus stack is not empty. 970 final int requestResult = pushBelowLockedFocusOwners(nfr); 971 if (requestResult != AudioManager.AUDIOFOCUS_REQUEST_FAILED) { 972 notifyExtPolicyFocusGrant_syncAf(nfr.toAudioFocusInfo(), requestResult); 973 } 974 return requestResult; 975 } else { 976 // propagate the focus change through the stack 977 propagateFocusLossFromGain_syncAf(focusChangeHint, nfr, forceDuck); 978 979 // push focus requester at the top of the audio focus stack 980 mFocusStack.push(nfr); 981 nfr.handleFocusGainFromRequest(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); 982 } 983 notifyExtPolicyFocusGrant_syncAf(nfr.toAudioFocusInfo(), 984 AudioManager.AUDIOFOCUS_REQUEST_GRANTED); 985 986 if (ENFORCE_MUTING_FOR_RING_OR_CALL & enteringRingOrCall) { 987 runAudioCheckerForRingOrCallAsync(true/*enteringRingOrCall*/); 988 } 989 }//synchronized(mAudioFocusLock) 990 991 return AudioManager.AUDIOFOCUS_REQUEST_GRANTED; 992 } 993 994 /** 995 * @see AudioManager#abandonAudioFocus(AudioManager.OnAudioFocusChangeListener, AudioAttributes) 996 * */ abandonAudioFocus(IAudioFocusDispatcher fl, String clientId, AudioAttributes aa, String callingPackageName)997 protected int abandonAudioFocus(IAudioFocusDispatcher fl, String clientId, AudioAttributes aa, 998 String callingPackageName) { 999 new MediaMetrics.Item(mMetricsId) 1000 .setUid(Binder.getCallingUid()) 1001 .set(MediaMetrics.Property.CALLING_PACKAGE, callingPackageName) 1002 .set(MediaMetrics.Property.CLIENT_NAME, clientId) 1003 .set(MediaMetrics.Property.EVENT, "abandonAudioFocus") 1004 .record(); 1005 1006 // AudioAttributes are currently ignored, to be used for zones / a11y 1007 mEventLogger.log((new AudioEventLogger.StringEvent( 1008 "abandonAudioFocus() from uid/pid " + Binder.getCallingUid() 1009 + "/" + Binder.getCallingPid() 1010 + " clientId=" + clientId)) 1011 .printLog(TAG)); 1012 try { 1013 // this will take care of notifying the new focus owner if needed 1014 synchronized(mAudioFocusLock) { 1015 // external focus policy? 1016 if (mFocusPolicy != null) { 1017 final AudioFocusInfo afi = new AudioFocusInfo(aa, Binder.getCallingUid(), 1018 clientId, callingPackageName, 0 /*gainRequest*/, 0 /*lossReceived*/, 1019 0 /*flags*/, 0 /* sdk n/a here*/); 1020 if (notifyExtFocusPolicyFocusAbandon_syncAf(afi)) { 1021 return AudioManager.AUDIOFOCUS_REQUEST_GRANTED; 1022 } 1023 } 1024 1025 boolean exitingRingOrCall = mRingOrCallActive 1026 & (AudioSystem.IN_VOICE_COMM_FOCUS_ID.compareTo(clientId) == 0); 1027 if (exitingRingOrCall) { mRingOrCallActive = false; } 1028 1029 removeFocusStackEntry(clientId, true /*signal*/, true /*notifyFocusFollowers*/); 1030 1031 if (ENFORCE_MUTING_FOR_RING_OR_CALL & exitingRingOrCall) { 1032 runAudioCheckerForRingOrCallAsync(false/*enteringRingOrCall*/); 1033 } 1034 } 1035 } catch (java.util.ConcurrentModificationException cme) { 1036 // Catching this exception here is temporary. It is here just to prevent 1037 // a crash seen when the "Silent" notification is played. This is believed to be fixed 1038 // but this try catch block is left just to be safe. 1039 Log.e(TAG, "FATAL EXCEPTION AudioFocus abandonAudioFocus() caused " + cme); 1040 cme.printStackTrace(); 1041 } 1042 1043 return AudioManager.AUDIOFOCUS_REQUEST_GRANTED; 1044 } 1045 1046 unregisterAudioFocusClient(String clientId)1047 protected void unregisterAudioFocusClient(String clientId) { 1048 synchronized(mAudioFocusLock) { 1049 removeFocusStackEntry(clientId, false, true /*notifyFocusFollowers*/); 1050 } 1051 } 1052 runAudioCheckerForRingOrCallAsync(final boolean enteringRingOrCall)1053 private void runAudioCheckerForRingOrCallAsync(final boolean enteringRingOrCall) { 1054 new Thread() { 1055 public void run() { 1056 if (enteringRingOrCall) { 1057 try { 1058 Thread.sleep(RING_CALL_MUTING_ENFORCEMENT_DELAY_MS); 1059 } catch (InterruptedException e) { 1060 e.printStackTrace(); 1061 } 1062 } 1063 synchronized (mAudioFocusLock) { 1064 // since the new thread starting running the state could have changed, so 1065 // we need to check again mRingOrCallActive, not enteringRingOrCall 1066 if (mRingOrCallActive) { 1067 mFocusEnforcer.mutePlayersForCall(USAGES_TO_MUTE_IN_RING_OR_CALL); 1068 } else { 1069 mFocusEnforcer.unmutePlayersForCall(); 1070 } 1071 } 1072 } 1073 }.start(); 1074 } 1075 updateMultiAudioFocus(boolean enabled)1076 public void updateMultiAudioFocus(boolean enabled) { 1077 Log.d(TAG, "updateMultiAudioFocus( " + enabled + " )"); 1078 mMultiAudioFocusEnabled = enabled; 1079 Settings.System.putInt(mContext.getContentResolver(), 1080 Settings.System.MULTI_AUDIO_FOCUS_ENABLED, enabled ? 1 : 0); 1081 if (!mFocusStack.isEmpty()) { 1082 final FocusRequester fr = mFocusStack.peek(); 1083 fr.handleFocusLoss(AudioManager.AUDIOFOCUS_LOSS, null, false); 1084 } 1085 if (!enabled) { 1086 if (!mMultiAudioFocusList.isEmpty()) { 1087 for (FocusRequester multifr : mMultiAudioFocusList) { 1088 multifr.handleFocusLoss(AudioManager.AUDIOFOCUS_LOSS, null, false); 1089 } 1090 mMultiAudioFocusList.clear(); 1091 } 1092 } 1093 } 1094 getMultiAudioFocusEnabled()1095 public boolean getMultiAudioFocusEnabled() { 1096 return mMultiAudioFocusEnabled; 1097 } 1098 dumpMultiAudioFocus(PrintWriter pw)1099 private void dumpMultiAudioFocus(PrintWriter pw) { 1100 pw.println("Multi Audio Focus enabled :" + mMultiAudioFocusEnabled); 1101 if (!mMultiAudioFocusList.isEmpty()) { 1102 pw.println("Multi Audio Focus List:"); 1103 pw.println("------------------------------"); 1104 for (FocusRequester multifr : mMultiAudioFocusList) { 1105 multifr.dump(pw); 1106 } 1107 pw.println("------------------------------"); 1108 } 1109 } 1110 } 1111