1 /* 2 * Copyright (C) 2016 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.car; 18 19 import android.content.Context; 20 import android.media.AudioManager; 21 import android.media.IAudioService; 22 import android.media.IVolumeController; 23 import android.os.Handler; 24 import android.os.HandlerThread; 25 import android.os.Looper; 26 import android.os.Message; 27 import android.os.RemoteCallbackList; 28 import android.os.RemoteException; 29 import android.os.ServiceManager; 30 import android.provider.Settings; 31 import android.telecom.TelecomManager; 32 import android.util.ArrayMap; 33 import android.util.Log; 34 import android.util.SparseArray; 35 import android.view.KeyEvent; 36 37 import com.android.car.CarVolumeService.CarVolumeController; 38 import com.android.car.hal.AudioHalService; 39 import com.android.internal.annotations.GuardedBy; 40 41 import java.io.PrintWriter; 42 import java.util.Map; 43 44 /** 45 * A factory class to create {@link com.android.car.CarVolumeService.CarVolumeController} based 46 * on car properties. 47 */ 48 public class CarVolumeControllerFactory { 49 // STOPSHIP if true. 50 private static final boolean DBG = false; 51 52 public static CarVolumeController createCarVolumeController(Context context, 53 CarAudioService audioService, AudioHalService audioHal, CarInputService inputService) { 54 final boolean volumeSupported = audioHal.isAudioVolumeSupported(); 55 56 // Case 1: Car Audio Module does not support volume controls 57 if (!volumeSupported) { 58 return new SimpleCarVolumeController(context); 59 } 60 return new CarExternalVolumeController(context, audioService, audioHal, inputService); 61 } 62 63 public static boolean interceptVolKeyBeforeDispatching(Context context) { 64 Log.d(CarLog.TAG_AUDIO, "interceptVolKeyBeforeDispatching"); 65 66 TelecomManager telecomManager = (TelecomManager) 67 context.getSystemService(Context.TELECOM_SERVICE); 68 if (telecomManager != null && telecomManager.isRinging()) { 69 // If an incoming call is ringing, either VOLUME key means 70 // "silence ringer". This is consistent with current android phone's behavior 71 Log.i(CarLog.TAG_AUDIO, "interceptKeyBeforeQueueing:" 72 + " VOLUME key-down while ringing: Silence ringer!"); 73 74 // Silence the ringer. (It's safe to call this 75 // even if the ringer has already been silenced.) 76 telecomManager.silenceRinger(); 77 return true; 78 } 79 return false; 80 } 81 82 public static boolean isVolumeKey(KeyEvent event) { 83 return event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_DOWN 84 || event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP; 85 } 86 87 /** 88 * To control volumes through {@link android.media.AudioManager} when car audio module does not 89 * support volume controls. 90 */ 91 public static final class SimpleCarVolumeController extends CarVolumeController { 92 private final AudioManager mAudioManager; 93 private final Context mContext; 94 95 public SimpleCarVolumeController(Context context) { 96 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 97 mContext = context; 98 } 99 100 @Override 101 void init() { 102 } 103 104 @Override 105 void release() { 106 } 107 108 @Override 109 public void setStreamVolume(int stream, int index, int flags) { 110 if (DBG) { 111 Log.d(CarLog.TAG_AUDIO, "setStreamVolume " + stream + " " + index + " " + flags); 112 } 113 mAudioManager.setStreamVolume(stream, index, flags); 114 } 115 116 @Override 117 public int getStreamVolume(int stream) { 118 return mAudioManager.getStreamVolume(stream); 119 } 120 121 @Override 122 public void setVolumeController(IVolumeController controller) { 123 mAudioManager.setVolumeController(controller); 124 } 125 126 @Override 127 public int getStreamMaxVolume(int stream) { 128 return mAudioManager.getStreamMaxVolume(stream); 129 } 130 131 @Override 132 public int getStreamMinVolume(int stream) { 133 return mAudioManager.getStreamMinVolume(stream); 134 } 135 136 @Override 137 public boolean onKeyEvent(KeyEvent event) { 138 if (!isVolumeKey(event)) { 139 return false; 140 } 141 handleVolumeKeyDefault(event); 142 return true; 143 } 144 145 @Override 146 public void dump(PrintWriter writer) { 147 writer.println("Volume controller:" + SimpleCarVolumeController.class.getSimpleName()); 148 // nothing else to dump 149 } 150 151 private void handleVolumeKeyDefault(KeyEvent event) { 152 if (event.getAction() != KeyEvent.ACTION_DOWN 153 || interceptVolKeyBeforeDispatching(mContext)) { 154 return; 155 } 156 157 boolean volUp = event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP; 158 int flags = AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_PLAY_SOUND 159 | AudioManager.FLAG_FROM_KEY; 160 IAudioService audioService = getAudioService(); 161 String pkgName = mContext.getOpPackageName(); 162 try { 163 if (audioService != null) { 164 audioService.adjustSuggestedStreamVolume( 165 volUp ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER, 166 AudioManager.USE_DEFAULT_STREAM_TYPE, flags, pkgName, CarLog.TAG_INPUT); 167 } 168 } catch (RemoteException e) { 169 Log.e(CarLog.TAG_INPUT, "Error calling android audio service.", e); 170 } 171 } 172 173 private static IAudioService getAudioService() { 174 IAudioService audioService = IAudioService.Stub.asInterface( 175 ServiceManager.checkService(Context.AUDIO_SERVICE)); 176 if (audioService == null) { 177 Log.w(CarLog.TAG_INPUT, "Unable to find IAudioService interface."); 178 } 179 return audioService; 180 } 181 } 182 183 /** 184 * The car volume controller to use when the car audio modules supports volume controls. 185 * 186 * Depending on whether the car support audio context and has persistent memory, we need to 187 * handle per context volume change properly. 188 * 189 * Regardless whether car supports audio context or not, we need to keep per audio context 190 * volume internally. If we only support single channel, then we only send the volume change 191 * event when that stream is in focus; Otherwise, we need to adjust the stream volume either on 192 * software mixer level or send it the car audio module if the car support audio context 193 * and multi channel. TODO: Add support for multi channel. bug: 32095376 194 * 195 * Per context volume should be persisted, so the volumes can stay the same across boots. 196 * Depending on the hardware property, this can be persisted on car side (or/and android side). 197 * TODO: we need to define one single source of truth if the car has memory. bug: 32091839 198 */ 199 public static class CarExternalVolumeController extends CarVolumeController 200 implements CarInputService.KeyEventListener, AudioHalService.AudioHalVolumeListener, 201 CarAudioService.AudioContextChangeListener { 202 private static final String TAG = CarLog.TAG_AUDIO + ".VolCtrl"; 203 private static final int MSG_UPDATE_VOLUME = 0; 204 private static final int MSG_UPDATE_HAL = 1; 205 private static final int MSG_SUPPRESS_UI_FOR_VOLUME = 2; 206 private static final int MSG_VOLUME_UI_RESTORE = 3; 207 208 // within 5 seconds after a UI invisible volume change (e.g., due to audio context change, 209 // or explicitly flag), we will not show UI in respond to that particular volume changes 210 // events from HAL (context and volume index must match). 211 private static final int HIDE_VOLUME_UI_MILLISECONDS = 5 * 1000; // 5 seconds 212 213 private final Context mContext; 214 private final AudioRoutingPolicy mPolicy; 215 private final AudioHalService mHal; 216 private final CarInputService mInputService; 217 private final CarAudioService mAudioService; 218 219 private int mSupportedAudioContext; 220 221 private boolean mHasExternalMemory; 222 private boolean mMasterVolumeOnly; 223 224 @GuardedBy("this") 225 private int mCurrentContext = CarVolumeService.DEFAULT_CAR_AUDIO_CONTEXT; 226 // current logical volume, the key is car audio context 227 @GuardedBy("this") 228 private final SparseArray<Integer> mCurrentCarContextVolume = 229 new SparseArray<>(VolumeUtils.CAR_AUDIO_CONTEXT.length); 230 // stream volume limit, the key is car audio context type 231 @GuardedBy("this") 232 private final SparseArray<Integer> mCarContextVolumeMax = 233 new SparseArray<>(VolumeUtils.CAR_AUDIO_CONTEXT.length); 234 // stream volume limit, the key is car audio context type 235 @GuardedBy("this") 236 private final RemoteCallbackList<IVolumeController> mVolumeControllers = 237 new RemoteCallbackList<>(); 238 @GuardedBy("this") 239 private int[] mSuppressUiForVolume = new int[2]; 240 @GuardedBy("this") 241 private boolean mShouldSuppress = false; 242 private HandlerThread mVolumeThread; 243 private Handler mHandler; 244 245 /** 246 * Convert an car context to the car stream. 247 * 248 * @return If car supports audio context, then it returns the car audio context. Otherwise, 249 * it returns the physical stream that maps to this logical stream. 250 */ 251 private int carContextToCarStream(int carContext) { 252 if (mSupportedAudioContext == 0) { 253 int physicalStream = mPolicy.getPhysicalStreamForLogicalStream( 254 AudioHalService.carContextToCarUsage(carContext)); 255 return physicalStream; 256 } else { 257 return carContext; 258 } 259 } 260 261 private void writeVolumeToSettings(int carContext, int volume) { 262 String key = VolumeUtils.CAR_AUDIO_CONTEXT_SETTINGS.get(carContext); 263 if (key != null) { 264 Settings.Global.putInt(mContext.getContentResolver(), key, volume); 265 } 266 } 267 268 /** 269 * All updates to external components should be posted to this handler to avoid holding 270 * the internal lock while sending updates. 271 */ 272 private final class VolumeHandler extends Handler { 273 public VolumeHandler(Looper looper) { 274 super(looper); 275 } 276 @Override 277 public void handleMessage(Message msg) { 278 int stream; 279 int volume; 280 switch (msg.what) { 281 case MSG_UPDATE_VOLUME: 282 // arg1 is car context 283 stream = msg.arg1; 284 volume = (int) msg.obj; 285 int flag = msg.arg2; 286 synchronized (CarExternalVolumeController.this) { 287 // the suppressed stream is sending us update.... 288 if (mShouldSuppress && stream == mSuppressUiForVolume[0]) { 289 // the volume matches, we want to suppress it 290 if (volume == mSuppressUiForVolume[1]) { 291 if (DBG) { 292 Log.d(TAG, "Suppress Volume UI for stream " 293 + stream + " volume: " + volume); 294 } 295 flag &= ~AudioManager.FLAG_SHOW_UI; 296 } 297 // No matter if the volume matches or not, we will stop suppressing 298 // UI for this stream now. After an audio context switch, user may 299 // quickly turn the nob, -1 and +1, it ends the same volume, 300 // but we should show the UI for both. 301 removeMessages(MSG_VOLUME_UI_RESTORE); 302 mShouldSuppress = false; 303 } 304 } 305 final int size = mVolumeControllers.beginBroadcast(); 306 try { 307 for (int i = 0; i < size; i++) { 308 try { 309 mVolumeControllers.getBroadcastItem(i).volumeChanged( 310 VolumeUtils.carContextToAndroidStream(stream), flag); 311 } catch (RemoteException ignored) { 312 } 313 } 314 } finally { 315 mVolumeControllers.finishBroadcast(); 316 } 317 break; 318 case MSG_UPDATE_HAL: 319 stream = msg.arg1; 320 volume = msg.arg2; 321 synchronized (CarExternalVolumeController.this) { 322 if (mMasterVolumeOnly) { 323 stream = 0; 324 } 325 } 326 mHal.setStreamVolume(stream, volume); 327 break; 328 case MSG_SUPPRESS_UI_FOR_VOLUME: 329 if (DBG) { 330 Log.d(TAG, "Suppress stream volume " + msg.arg1 + " " + msg.arg2); 331 } 332 synchronized (CarExternalVolumeController.this) { 333 mShouldSuppress = true; 334 mSuppressUiForVolume[0] = msg.arg1; 335 mSuppressUiForVolume[1] = msg.arg2; 336 } 337 removeMessages(MSG_VOLUME_UI_RESTORE); 338 sendMessageDelayed(obtainMessage(MSG_VOLUME_UI_RESTORE), 339 HIDE_VOLUME_UI_MILLISECONDS); 340 break; 341 case MSG_VOLUME_UI_RESTORE: 342 if (DBG) { 343 Log.d(TAG, "Volume Ui suppress expired"); 344 } 345 synchronized (CarExternalVolumeController.this) { 346 mShouldSuppress = false; 347 } 348 break; 349 default: 350 break; 351 } 352 } 353 } 354 355 public CarExternalVolumeController(Context context, CarAudioService audioService, 356 AudioHalService hal, CarInputService inputService) { 357 mContext = context; 358 mAudioService = audioService; 359 mPolicy = audioService.getAudioRoutingPolicy(); 360 mHal = hal; 361 mInputService = inputService; 362 } 363 364 @Override 365 void init() { 366 mSupportedAudioContext = mHal.getSupportedAudioVolumeContexts(); 367 mHasExternalMemory = mHal.isExternalAudioVolumePersistent(); 368 mMasterVolumeOnly = mHal.isAudioVolumeMasterOnly(); 369 synchronized (this) { 370 mVolumeThread = new HandlerThread(TAG); 371 mVolumeThread.start(); 372 mHandler = new VolumeHandler(mVolumeThread.getLooper()); 373 initVolumeLimitLocked(); 374 initCurrentVolumeLocked(); 375 } 376 mInputService.setVolumeKeyListener(this); 377 mHal.setVolumeListener(this); 378 mAudioService.setAudioContextChangeListener(Looper.getMainLooper(), this); 379 } 380 381 @Override 382 void release() { 383 synchronized (this) { 384 if (mVolumeThread != null) { 385 mVolumeThread.quit(); 386 } 387 } 388 } 389 390 private void initVolumeLimitLocked() { 391 for (int i : VolumeUtils.CAR_AUDIO_CONTEXT) { 392 int carStream = carContextToCarStream(i); 393 Integer volumeMax = mHal.getStreamMaxVolume(carStream); 394 int max = volumeMax == null ? 0 : volumeMax; 395 if (max < 0) { 396 max = 0; 397 } 398 // get default stream volume limit first. 399 mCarContextVolumeMax.put(i, max); 400 } 401 } 402 403 private void initCurrentVolumeLocked() { 404 if (mHasExternalMemory) { 405 // TODO: read per context volume from audio hal. bug: 32091839 406 } else { 407 // when vhal does not work, get call can take long. For that case, 408 // for the same physical streams, cache initial get results 409 Map<Integer, Integer> volumesPerCarStream = 410 new ArrayMap<>(VolumeUtils.CAR_AUDIO_CONTEXT.length); 411 for (int i : VolumeUtils.CAR_AUDIO_CONTEXT) { 412 String key = VolumeUtils.CAR_AUDIO_CONTEXT_SETTINGS.get(i); 413 if (key != null) { 414 int vol = Settings.Global.getInt(mContext.getContentResolver(), key, -1); 415 if (vol >= 0) { 416 // Read valid volume for this car context from settings and continue; 417 mCurrentCarContextVolume.put(i, vol); 418 if (DBG) { 419 Log.d(TAG, "init volume from settings, car audio context: " 420 + i + " volume: " + vol); 421 } 422 continue; 423 } 424 } 425 426 // There is no settings for this car context. Use the current physical car 427 // stream volume as initial value instead, and put the volume into settings. 428 int carStream = carContextToCarStream(i); 429 Integer volume = volumesPerCarStream.get(carStream); 430 if (volume == null) { 431 volume = Integer.valueOf(mHal.getStreamVolume(mMasterVolumeOnly ? 0 : 432 carStream)); 433 volumesPerCarStream.put(carStream, volume); 434 } 435 mCurrentCarContextVolume.put(i, volume); 436 writeVolumeToSettings(i, volume); 437 if (DBG) { 438 Log.d(TAG, "init volume from physical stream," + 439 " car audio context: " + i + " volume: " + volume); 440 } 441 } 442 } 443 } 444 445 @Override 446 public void setStreamVolume(int stream, int index, int flags) { 447 synchronized (this) { 448 int carContext; 449 // Currently car context and android logical stream are not 450 // one-to-one mapping. In this API, Android side asks us to change a logical stream 451 // volume. If the current car audio context maps to this logical stream, then we 452 // change the volume for the current car audio context. Otherwise, we change the 453 // volume for the primary mapped car audio context. 454 if (VolumeUtils.carContextToAndroidStream(mCurrentContext) == stream) { 455 carContext = mCurrentContext; 456 } else { 457 carContext = VolumeUtils.androidStreamToCarContext(stream); 458 } 459 if (DBG) { 460 Log.d(TAG, "Receive setStreamVolume logical stream: " + stream + " index: " 461 + index + " flags: " + flags + " maps to car context: " + carContext); 462 } 463 setStreamVolumeInternalLocked(carContext, index, flags); 464 } 465 } 466 467 private void setStreamVolumeInternalLocked(int carContext, int index, int flags) { 468 if (mCarContextVolumeMax.get(carContext) == null) { 469 Log.e(TAG, "Stream type not supported " + carContext); 470 return; 471 } 472 int limit = mCarContextVolumeMax.get(carContext); 473 if (index > limit) { 474 Log.w(TAG, "Volume exceeds volume limit. context: " + carContext 475 + " index: " + index + " limit: " + limit); 476 index = limit; 477 } 478 479 if (index < 0) { 480 index = 0; 481 } 482 483 if (mCurrentCarContextVolume.get(carContext) == index) { 484 return; 485 } 486 487 int carStream = carContextToCarStream(carContext); 488 if (DBG) { 489 Log.d(TAG, "Change car stream volume, stream: " + carStream + " volume:" + index); 490 } 491 // For single channel, only adjust the volume when the audio context is the current one. 492 if (mCurrentContext == carContext) { 493 if (DBG) { 494 Log.d(TAG, "Sending volume change to HAL"); 495 } 496 mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_HAL, carStream, index)); 497 if ((flags & AudioManager.FLAG_SHOW_UI) != 0) { 498 if (mShouldSuppress && mSuppressUiForVolume[0] == carContext) { 499 // In this case, the caller explicitly says "Show_UI" for the same context. 500 // We will respect the flag, and let the UI show. 501 mShouldSuppress = false; 502 mHandler.removeMessages(MSG_VOLUME_UI_RESTORE); 503 } 504 } else { 505 mHandler.sendMessage(mHandler.obtainMessage(MSG_SUPPRESS_UI_FOR_VOLUME, 506 carContext, index)); 507 } 508 } 509 // Record the current volume internally. 510 mCurrentCarContextVolume.put(carContext, index); 511 writeVolumeToSettings(mCurrentContext, index); 512 } 513 514 @Override 515 public int getStreamVolume(int stream) { 516 synchronized (this) { 517 if (VolumeUtils.carContextToAndroidStream(mCurrentContext) == stream) { 518 return mCurrentCarContextVolume.get(mCurrentContext); 519 } 520 return mCurrentCarContextVolume.get(VolumeUtils.androidStreamToCarContext(stream)); 521 } 522 } 523 524 @Override 525 public void setVolumeController(IVolumeController controller) { 526 synchronized (this) { 527 mVolumeControllers.register(controller); 528 } 529 } 530 531 @Override 532 public void onVolumeChange(int carStream, int volume, int volumeState) { 533 synchronized (this) { 534 int flag = getVolumeUpdateFlag(true); 535 if (DBG) { 536 Log.d(TAG, "onVolumeChange carStream:" + carStream + " volume: " + volume 537 + " volumeState: " + volumeState 538 + " suppressUI? " + mShouldSuppress 539 + " stream: " + mSuppressUiForVolume[0] 540 + " volume: " + mSuppressUiForVolume[1]); 541 } 542 int currentCarStream = carContextToCarStream(mCurrentContext); 543 if (mMasterVolumeOnly) { //for master volume only H/W, always assume current stream 544 carStream = currentCarStream; 545 } 546 if (currentCarStream == carStream) { 547 mCurrentCarContextVolume.put(mCurrentContext, volume); 548 writeVolumeToSettings(mCurrentContext, volume); 549 mHandler.sendMessage( 550 mHandler.obtainMessage(MSG_UPDATE_VOLUME, mCurrentContext, flag, 551 new Integer(volume))); 552 } else { 553 // Hal is telling us a car stream volume has changed, but it is not the current 554 // stream. 555 Log.w(TAG, "Car stream" + carStream 556 + " volume changed, but it is not current stream, ignored."); 557 } 558 } 559 } 560 561 private int getVolumeUpdateFlag(boolean showUi) { 562 return showUi? AudioManager.FLAG_SHOW_UI : 0; 563 } 564 565 @Override 566 public void onVolumeLimitChange(int streamNumber, int volume) { 567 // TODO: How should this update be sent to SystemUI? bug: 32095237 568 // maybe send a volume update without showing UI. 569 synchronized (this) { 570 initVolumeLimitLocked(); 571 } 572 } 573 574 @Override 575 public int getStreamMaxVolume(int stream) { 576 synchronized (this) { 577 if (VolumeUtils.carContextToAndroidStream(mCurrentContext) == stream) { 578 return mCarContextVolumeMax.get(mCurrentContext); 579 } else { 580 return mCarContextVolumeMax.get(VolumeUtils.androidStreamToCarContext(stream)); 581 } 582 } 583 } 584 585 @Override 586 public int getStreamMinVolume(int stream) { 587 return 0; // Min value is always zero. 588 } 589 590 @Override 591 public boolean onKeyEvent(KeyEvent event) { 592 if (!isVolumeKey(event)) { 593 return false; 594 } 595 final boolean down = event.getAction() == KeyEvent.ACTION_DOWN; 596 if (DBG) { 597 Log.d(TAG, "Receive volume keyevent " + event.toString()); 598 } 599 // TODO: properly handle long press on volume key, bug: 32095989 600 if (!down || interceptVolKeyBeforeDispatching(mContext)) { 601 return true; 602 } 603 604 synchronized (this) { 605 int currentVolume = mCurrentCarContextVolume.get(mCurrentContext); 606 switch (event.getKeyCode()) { 607 case KeyEvent.KEYCODE_VOLUME_UP: 608 setStreamVolumeInternalLocked(mCurrentContext, currentVolume + 1, 609 getVolumeUpdateFlag(true)); 610 break; 611 case KeyEvent.KEYCODE_VOLUME_DOWN: 612 setStreamVolumeInternalLocked(mCurrentContext, currentVolume - 1, 613 getVolumeUpdateFlag(true)); 614 break; 615 } 616 } 617 return true; 618 } 619 620 @Override 621 public void onContextChange(int primaryFocusContext, int primaryFocusPhysicalStream) { 622 synchronized (this) { 623 if(DBG) { 624 Log.d(TAG, "Audio context changed from " + mCurrentContext + " to: " 625 + primaryFocusContext + " physical: " + primaryFocusPhysicalStream); 626 } 627 // if primaryFocusContext is 0, it means nothing is playing or holding focus, 628 // we will keep the last focus context and if the user changes the volume 629 // it will go to the last audio context. 630 if (primaryFocusContext == mCurrentContext || primaryFocusContext == 0) { 631 return; 632 } 633 int oldContext = mCurrentContext; 634 mCurrentContext = primaryFocusContext; 635 // if car supports audio context and has external memory, then we don't need to do 636 // anything. 637 if(mSupportedAudioContext != 0 && mHasExternalMemory) { 638 if (DBG) { 639 Log.d(TAG, "Car support audio context and has external memory," + 640 " no volume change needed from car service"); 641 } 642 return; 643 } 644 645 // Otherwise, we need to tell Hal what the correct volume is for the new context. 646 int currentVolume = mCurrentCarContextVolume.get(primaryFocusContext); 647 648 int carStreamNumber = (mSupportedAudioContext == 0) ? primaryFocusPhysicalStream : 649 primaryFocusContext; 650 if (DBG) { 651 Log.d(TAG, "Change volume from: " 652 + mCurrentCarContextVolume.get(oldContext) 653 + " to: "+ currentVolume); 654 } 655 mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_HAL, carStreamNumber, 656 currentVolume)); 657 mHandler.sendMessage(mHandler.obtainMessage(MSG_SUPPRESS_UI_FOR_VOLUME, 658 mCurrentContext, currentVolume)); 659 } 660 } 661 662 @Override 663 public void dump(PrintWriter writer) { 664 writer.println("Volume controller:" + 665 CarExternalVolumeController.class.getSimpleName()); 666 synchronized (this) { 667 writer.println("mSupportedAudioContext:0x" + 668 Integer.toHexString(mSupportedAudioContext) + 669 ",mHasExternalMemory:" + mHasExternalMemory + 670 ",mMasterVolumeOnly:" + mMasterVolumeOnly); 671 writer.println("mCurrentContext:0x" + Integer.toHexString(mCurrentContext)); 672 writer.println("mCurrentCarContextVolume:"); 673 dumpVolumes(writer, mCurrentCarContextVolume); 674 writer.println("mCarContextVolumeMax:"); 675 dumpVolumes(writer, mCarContextVolumeMax); 676 writer.println("Number of volume controllers:" + 677 mVolumeControllers.getRegisteredCallbackCount()); 678 } 679 } 680 681 private void dumpVolumes(PrintWriter writer, SparseArray<Integer> array) { 682 for (int i = 0; i < array.size(); i++) { 683 writer.println("0x" + Integer.toHexString(array.keyAt(i)) + ":" + array.valueAt(i)); 684 } 685 } 686 } 687 } 688