CarVolumeControllerFactory.java revision c4d442f4a0d3acf90b1c7a1dd7c222a8f32a193f
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 17package com.android.car; 18 19import android.content.Context; 20import android.media.AudioManager; 21import android.media.IAudioService; 22import android.media.IVolumeController; 23import android.os.Handler; 24import android.os.Looper; 25import android.os.Message; 26import android.os.RemoteCallbackList; 27import android.os.RemoteException; 28import android.os.ServiceManager; 29import android.util.Log; 30import android.util.Pair; 31import android.util.SparseArray; 32import android.view.KeyEvent; 33 34import com.android.car.CarVolumeService.CarVolumeController; 35import com.android.car.hal.AudioHalService; 36import com.android.internal.annotations.GuardedBy; 37 38/** 39 * A factory class to create {@link com.android.car.CarVolumeService.CarVolumeController} based 40 * on car properties. 41 */ 42public class CarVolumeControllerFactory { 43 44 public static CarVolumeController createCarVolumeController(Context context, 45 CarAudioService audioService, AudioHalService audioHal, CarInputService inputService) { 46 final boolean volumeSupported = audioHal.isAudioVolumeSupported(); 47 48 // Case 1: Car Audio Module does not support volume controls 49 if (!volumeSupported) { 50 return new SimpleCarVolumeController(context); 51 } 52 return new CarExternalVolumeController(context, audioService, audioHal, inputService); 53 } 54 55 /** 56 * To control volumes through {@link android.media.AudioManager} when car audio module does not 57 * support volume controls. 58 */ 59 public static final class SimpleCarVolumeController extends CarVolumeController { 60 private final AudioManager mAudioManager; 61 private final Context mContext; 62 63 public SimpleCarVolumeController(Context context) { 64 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 65 mContext = context; 66 } 67 68 @Override 69 void init() { 70 } 71 72 @Override 73 public void setStreamVolume(int stream, int index, int flags) { 74 mAudioManager.setStreamVolume(stream, index, flags); 75 } 76 77 @Override 78 public int getStreamVolume(int stream) { 79 return mAudioManager.getStreamVolume(stream); 80 } 81 82 @Override 83 public void setVolumeController(IVolumeController controller) { 84 mAudioManager.setVolumeController(controller); 85 } 86 87 @Override 88 public int getStreamMaxVolume(int stream) { 89 return mAudioManager.getStreamMaxVolume(stream); 90 } 91 92 @Override 93 public int getStreamMinVolume(int stream) { 94 return mAudioManager.getStreamMinVolume(stream); 95 } 96 97 @Override 98 public boolean onKeyEvent(KeyEvent event) { 99 handleVolumeKeyDefault(event); 100 return true; 101 } 102 103 private void handleVolumeKeyDefault(KeyEvent event) { 104 if (event.getAction() != KeyEvent.ACTION_DOWN) { 105 return; 106 } 107 108 boolean volUp = event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP; 109 int flags = AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_PLAY_SOUND 110 | AudioManager.FLAG_FROM_KEY; 111 IAudioService audioService = getAudioService(); 112 String pkgName = mContext.getOpPackageName(); 113 try { 114 if (audioService != null) { 115 audioService.adjustSuggestedStreamVolume( 116 volUp ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER, 117 AudioManager.USE_DEFAULT_STREAM_TYPE, flags, pkgName, CarLog.TAG_INPUT); 118 } 119 } catch (RemoteException e) { 120 Log.e(CarLog.TAG_INPUT, "Error calling android audio service.", e); 121 } 122 } 123 124 private static IAudioService getAudioService() { 125 IAudioService audioService = IAudioService.Stub.asInterface( 126 ServiceManager.checkService(Context.AUDIO_SERVICE)); 127 if (audioService == null) { 128 Log.w(CarLog.TAG_INPUT, "Unable to find IAudioService interface."); 129 } 130 return audioService; 131 } 132 } 133 134 /** 135 * The car volume controller to use when the car audio modules supports volume controls. 136 * 137 * Depending on whether the car support audio context and has persistent memory, we need to 138 * handle per context volume change properly. 139 * 140 * Regardless whether car supports audio context or not, we need to keep per audio context 141 * volume internally. If we only support single channel, then we only send the volume change 142 * event when that stream is in focus; Otherwise, we need to adjust the stream volume either on 143 * software mixer level or send it the car audio module if the car support audio context 144 * and multi channel. TODO: Add support for multi channel. 145 * 146 * Per context volume should be persisted, so the volumes can stay the same across boots. 147 * Depending on the hardware property, this can be persisted on car side (or/and android side). 148 * TODO: we need to define one single source of truth if the car has memory. 149 */ 150 public static class CarExternalVolumeController extends CarVolumeController 151 implements CarInputService.KeyEventListener, AudioHalService.AudioHalVolumeListener, 152 CarAudioService.AudioContextChangeListener { 153 private static final String TAG = CarLog.TAG_AUDIO + "ExtVolCtrl"; 154 private static final int MSG_UPDATE_VOLUME = 0; 155 private static final int MSG_UPDATE_HAL = 1; 156 157 private final Context mContext; 158 private final AudioRoutingPolicy mPolicy; 159 private final AudioHalService mHal; 160 private final CarInputService mInputService; 161 private final CarAudioService mAudioService; 162 163 private int mSupportedAudioContext; 164 165 private boolean mHasExternalMemory; 166 167 @GuardedBy("this") 168 private int mCurrentContext = CarVolumeService.DEFAULT_CAR_AUDIO_CONTEXT; 169 // current logical volume, the key is android stream type 170 @GuardedBy("this") 171 private final SparseArray<Integer> mCurrentLogicalVolume = 172 new SparseArray<>(VolumeUtils.LOGICAL_STREAMS.length); 173 // stream volume limit, the key is android stream type 174 @GuardedBy("this") 175 private final SparseArray<Integer> mLogicalStreamVolumeMax = 176 new SparseArray<>(VolumeUtils.LOGICAL_STREAMS.length); 177 // stream volume limit, the key is android stream type 178 @GuardedBy("this") 179 private final SparseArray<Integer> mLogicalStreamVolumeMin = 180 new SparseArray<>(VolumeUtils.LOGICAL_STREAMS.length); 181 182 @GuardedBy("this") 183 private final RemoteCallbackList<IVolumeController> mVolumeControllers = 184 new RemoteCallbackList<>(); 185 186 private final Handler mHandler = new VolumeHandler(); 187 188 /** 189 * Convert an android logical stream to the car stream. 190 * 191 * @return If car supports audio context, then it returns the car audio context. Otherwise, 192 * it returns the physical stream that maps to this logical stream. 193 */ 194 private int logicalStreamToCarStream(int logicalAndroidStream) { 195 if (mSupportedAudioContext == 0) { 196 int physicalStream = mPolicy.getPhysicalStreamForLogicalStream( 197 CarVolumeService.androidStreamToCarUsage(logicalAndroidStream)); 198 return physicalStream; 199 } else { 200 int carContext = VolumeUtils.androidStreamToCarContext(logicalAndroidStream); 201 if ((carContext & mSupportedAudioContext) == 0) { 202 carContext = CarVolumeService.DEFAULT_CAR_AUDIO_CONTEXT; 203 } 204 return carContext; 205 } 206 } 207 208 /** 209 * All updates to external components should be posted to this handler to avoid holding 210 * the internal lock while sending updates. 211 */ 212 private final class VolumeHandler extends Handler { 213 @Override 214 public void handleMessage(Message msg) { 215 int stream; 216 int volume; 217 switch (msg.what) { 218 case MSG_UPDATE_VOLUME: 219 stream = msg.arg1; 220 int flag = msg.arg2; 221 final int size = mVolumeControllers.beginBroadcast(); 222 try { 223 for (int i = 0; i < size; i++) { 224 try { 225 mVolumeControllers.getBroadcastItem(i) 226 .volumeChanged(stream, flag); 227 } catch (RemoteException ignored) { 228 } 229 } 230 } finally { 231 mVolumeControllers.finishBroadcast(); 232 } 233 break; 234 case MSG_UPDATE_HAL: 235 stream = msg.arg1; 236 volume = msg.arg2; 237 mHal.setStreamVolume(stream, volume); 238 break; 239 default: 240 break; 241 } 242 } 243 } 244 245 public CarExternalVolumeController(Context context, CarAudioService audioService, 246 AudioHalService hal, CarInputService inputService) { 247 mContext = context; 248 mAudioService = audioService; 249 mPolicy = audioService.getAudioRoutingPolicy(); 250 mHal = hal; 251 mInputService = inputService; 252 } 253 254 @Override 255 void init() { 256 mSupportedAudioContext = mHal.getSupportedAudioVolumeContexts(); 257 mHasExternalMemory = mHal.isExternalAudioVolumePersistent(); 258 synchronized (this) { 259 initVolumeLimitLocked(); 260 initCurrentVolumeLocked(); 261 } 262 mInputService.setVolumeKeyListener(this); 263 mHal.setVolumeListener(this); 264 mAudioService.setAudioContextChangeListener(Looper.getMainLooper(), this); 265 } 266 267 private void initVolumeLimitLocked() { 268 for (int i : VolumeUtils.LOGICAL_STREAMS) { 269 int carStream = logicalStreamToCarStream(i); 270 Pair<Integer, Integer> volumeMinMax = mHal.getStreamVolumeLimit(carStream); 271 int max; 272 int min; 273 if (volumeMinMax == null) { 274 max = 0; 275 min = 0; 276 } else { 277 max = volumeMinMax.second >= 0 ? volumeMinMax.second : 0; 278 min = volumeMinMax.first >=0 ? volumeMinMax.first : 0; 279 } 280 // get default stream volume limit first. 281 mLogicalStreamVolumeMax.put(i, max); 282 mLogicalStreamVolumeMin.put(i, min); 283 } 284 } 285 286 private void initCurrentVolumeLocked() { 287 if (mHasExternalMemory) { 288 // TODO: read per context volume from audio hal 289 } else { 290 // TODO: read the Android side volume from Settings and pass it to the audio module 291 // Here we just set it to the physical stream volume temporarily. 292 for (int i : VolumeUtils.LOGICAL_STREAMS) { 293 mCurrentLogicalVolume.put(i, mHal.getStreamVolume(logicalStreamToCarStream(i))); 294 } 295 } 296 } 297 298 @Override 299 public void setStreamVolume(int stream, int index, int flags) { 300 synchronized (this) { 301 setStreamVolumeInternalLocked(stream, index, flags); 302 } 303 } 304 305 private void setStreamVolumeInternalLocked(int stream, int index, int flags) { 306 if (mLogicalStreamVolumeMax.get(stream) == null) { 307 Log.e(TAG, "Stream type not supported " + stream); 308 return; 309 } 310 int limit = mLogicalStreamVolumeMax.get(stream); 311 if (index > limit) { 312 Log.e(TAG, "Volume exceeds volume limit. stream: " + stream + " index: " + index 313 + " limit: " + limit); 314 index = limit; 315 } 316 317 if (index < 0) { 318 index = 0; 319 } 320 321 if (mCurrentLogicalVolume.get(stream) == index) { 322 return; 323 } 324 325 int carStream = logicalStreamToCarStream(stream); 326 int carContext = VolumeUtils.androidStreamToCarContext(stream); 327 328 // For single channel, only adjust the volume when the audio context is the current one. 329 if (mCurrentContext == carContext) { 330 mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_HAL, carStream, index)); 331 } 332 // Record the current volume internally. 333 mCurrentLogicalVolume.put(stream, index); 334 mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_VOLUME, stream, 335 getVolumeUpdateFlag())); 336 } 337 338 @Override 339 public int getStreamVolume(int stream) { 340 synchronized (this) { 341 if (mCurrentLogicalVolume.get(stream) == null) { 342 Log.d(TAG, "Invalid stream type " + stream); 343 return 0; 344 } 345 return mCurrentLogicalVolume.get(stream); 346 } 347 } 348 349 @Override 350 public void setVolumeController(IVolumeController controller) { 351 synchronized (this) { 352 mVolumeControllers.register(controller); 353 } 354 } 355 356 @Override 357 public void onVolumeChange(int carStream, int volume, int volumeState) { 358 int flag = getVolumeUpdateFlag(); 359 synchronized (this) { 360 // Assume single channel here. 361 int currentLogicalStream = VolumeUtils.carContextToAndroidStream(mCurrentContext); 362 int currentCarStream = logicalStreamToCarStream(currentLogicalStream); 363 if (currentCarStream == carStream) { 364 mCurrentLogicalVolume.put(currentLogicalStream, volume); 365 mHandler.sendMessage( 366 mHandler.obtainMessage(MSG_UPDATE_VOLUME, currentLogicalStream, flag)); 367 } else { 368 // Hal is telling us a car stream volume has changed, but it is not the current 369 // stream. 370 Log.w(TAG, "Car stream" + carStream 371 + " volume changed, but it is not current stream, ignored."); 372 } 373 } 374 } 375 376 private int getVolumeUpdateFlag() { 377 // TODO: Apply appropriate flags. 378 return AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_PLAY_SOUND; 379 } 380 381 private void updateHalVolumeLocked(final int carStream, final int index) { 382 mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_HAL, carStream, index)); 383 } 384 385 @Override 386 public void onVolumeLimitChange(int streamNumber, int volume) { 387 // TODO: How should this update be sent to SystemUI? maybe send a volume update without 388 // showing UI. 389 synchronized (this) { 390 initVolumeLimitLocked(); 391 } 392 } 393 394 @Override 395 public int getStreamMaxVolume(int stream) { 396 synchronized (this) { 397 if (mLogicalStreamVolumeMax.get(stream) == null) { 398 Log.e(TAG, "Stream type not supported " + stream); 399 return 0; 400 } 401 return mLogicalStreamVolumeMax.get(stream); 402 } 403 } 404 405 @Override 406 public int getStreamMinVolume(int stream) { 407 synchronized (this) { 408 if (mLogicalStreamVolumeMin.get(stream) == null) { 409 Log.e(TAG, "Stream type not supported " + stream); 410 return 0; 411 } 412 return mLogicalStreamVolumeMin.get(stream); 413 } 414 } 415 416 @Override 417 public boolean onKeyEvent(KeyEvent event) { 418 int logicalStream = VolumeUtils.carContextToAndroidStream(mCurrentContext); 419 final boolean down = event.getAction() == KeyEvent.ACTION_DOWN; 420 // TODO: properly handle long press on volume key 421 if (!down) { 422 return true; 423 } 424 425 synchronized (this) { 426 int currentVolume = mCurrentLogicalVolume.get(logicalStream); 427 switch (event.getKeyCode()) { 428 case KeyEvent.KEYCODE_VOLUME_UP: 429 setStreamVolumeInternalLocked(logicalStream, currentVolume + 1, 430 getVolumeUpdateFlag()); 431 break; 432 case KeyEvent.KEYCODE_VOLUME_DOWN: 433 setStreamVolumeInternalLocked(logicalStream, currentVolume - 1, 434 getVolumeUpdateFlag()); 435 break; 436 } 437 } 438 return true; 439 } 440 441 @Override 442 public void onContextChange(int primaryFocusContext, int primaryFocusPhysicalStream) { 443 synchronized (this) { 444 if (primaryFocusContext == mCurrentContext) { 445 return; 446 } 447 mCurrentContext = primaryFocusContext; 448 449 int currentVolume = mCurrentLogicalVolume.get( 450 VolumeUtils.carContextToAndroidStream(primaryFocusContext)); 451 if (mSupportedAudioContext == 0) { 452 // Car does not support audio context, we need to reset the volume 453 updateHalVolumeLocked(primaryFocusPhysicalStream, currentVolume); 454 } else { 455 // car supports context, but does not have memory. 456 if (!mHasExternalMemory) { 457 updateHalVolumeLocked(primaryFocusContext, currentVolume); 458 } 459 } 460 } 461 } 462 } 463} 464