1 /* 2 * Copyright (C) 2020 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.audio.hal; 18 19 import static android.media.AudioManager.AUDIOFOCUS_LOSS; 20 import static android.media.AudioManager.AUDIOFOCUS_REQUEST_DELAYED; 21 import static android.media.AudioManager.AUDIOFOCUS_REQUEST_FAILED; 22 import static android.media.AudioManager.AUDIOFOCUS_REQUEST_GRANTED; 23 24 import static com.android.car.audio.CarAudioDumpProto.HalAudioFocusProto.HAL_FOCUS_REQUESTS_BY_ZONE_AND_ATTRIBUTES; 25 import static com.android.car.audio.CarAudioDumpProto.HalAudioFocusProto.HalFocusRequestsByZoneAndAttributes.HAL_FOCUS_REQUESTS_BY_ATTRIBUTES; 26 import static com.android.car.audio.CarAudioDumpProto.HalAudioFocusProto.HalFocusRequestsByZoneAndAttributes.ZONE_ID; 27 import static com.android.car.audio.CarHalAudioUtils.audioAttributesWrapperToMetadata; 28 import static com.android.car.audio.CarHalAudioUtils.metadataToAudioAttribute; 29 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE; 30 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 31 32 import android.annotation.Nullable; 33 import android.car.builtin.util.Slogf; 34 import android.car.media.CarAudioManager; 35 import android.hardware.audio.common.PlaybackTrackMetadata; 36 import android.media.AudioAttributes; 37 import android.media.AudioFocusRequest; 38 import android.os.Binder; 39 import android.os.Bundle; 40 import android.util.ArrayMap; 41 import android.util.ArraySet; 42 import android.util.Log; 43 import android.util.Pair; 44 import android.util.SparseArray; 45 import android.util.proto.ProtoOutputStream; 46 47 import com.android.car.CarLog; 48 import com.android.car.audio.AudioManagerWrapper; 49 import com.android.car.audio.CarAudioContext; 50 import com.android.car.audio.CarAudioContext.AudioAttributesWrapper; 51 import com.android.car.audio.CarAudioDumpProto; 52 import com.android.car.audio.CarAudioPlaybackMonitor; 53 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 54 import com.android.car.internal.util.IndentingPrintWriter; 55 import com.android.internal.annotations.GuardedBy; 56 import com.android.internal.util.Preconditions; 57 58 import java.util.ArrayList; 59 import java.util.List; 60 import java.util.Map; 61 import java.util.Objects; 62 import java.util.Set; 63 64 /** 65 * Manages focus requests from the HAL on a per-zone per-usage basis 66 */ 67 public final class HalAudioFocus implements HalFocusListener { 68 private static final String TAG = CarLog.tagFor(HalAudioFocus.class); 69 70 private final AudioManagerWrapper mAudioManager; 71 private final AudioControlWrapper mAudioControlWrapper; 72 private final CarAudioContext mCarAudioContext; 73 @Nullable 74 private final CarAudioPlaybackMonitor mCarAudioPlaybackMonitor; 75 76 private final Object mLock = new Object(); 77 78 // Map of Maps. Top level keys are ZoneIds. Second level keys are audio attribute wrapper. 79 // Values are HalAudioFocusRequests 80 @GuardedBy("mLock") 81 private final SparseArray<Map<AudioAttributesWrapper, HalAudioFocusRequest>> 82 mHalFocusRequestsByZoneAndAttributes; 83 HalAudioFocus(AudioManagerWrapper audioManager, AudioControlWrapper audioControlWrapper, @Nullable CarAudioPlaybackMonitor carAudioPlaybackMonitor, CarAudioContext carAudioContext, int[] audioZoneIds)84 public HalAudioFocus(AudioManagerWrapper audioManager, 85 AudioControlWrapper audioControlWrapper, 86 @Nullable CarAudioPlaybackMonitor carAudioPlaybackMonitor, 87 CarAudioContext carAudioContext, int[] audioZoneIds) { 88 mAudioManager = Objects.requireNonNull(audioManager); 89 mAudioControlWrapper = Objects.requireNonNull(audioControlWrapper); 90 mCarAudioContext = Objects.requireNonNull(carAudioContext); 91 mCarAudioPlaybackMonitor = carAudioPlaybackMonitor; 92 Objects.requireNonNull(audioZoneIds, "Audio zone ID's can not be null"); 93 94 mHalFocusRequestsByZoneAndAttributes = new SparseArray<>(audioZoneIds.length); 95 for (int index = 0; index < audioZoneIds.length; index++) { 96 mHalFocusRequestsByZoneAndAttributes.put(audioZoneIds[index], new ArrayMap<>()); 97 } 98 } 99 100 /** 101 * Registers {@code IFocusListener} on {@code AudioControlWrapper} to receive HAL audio focus 102 * request and abandon calls. 103 */ registerFocusListener()104 public void registerFocusListener() { 105 mAudioControlWrapper.registerFocusListener(this); 106 } 107 108 /** 109 * Unregisters {@code IFocusListener} from {@code AudioControlWrapper}. 110 */ unregisterFocusListener()111 public void unregisterFocusListener() { 112 mAudioControlWrapper.unregisterFocusListener(); 113 } 114 115 /** 116 * See {@link HalFocusListener#requestAudioFocus(PlaybackTrackMetadata, int, int)} 117 */ requestAudioFocus(PlaybackTrackMetadata metadata, int zoneId, int focusGain)118 public void requestAudioFocus(PlaybackTrackMetadata metadata, int zoneId, int focusGain) { 119 synchronized (mLock) { 120 Preconditions.checkArgument(mHalFocusRequestsByZoneAndAttributes.contains(zoneId), 121 "Invalid zoneId %d provided in requestAudioFocus", zoneId); 122 AudioAttributes attributes = metadataToAudioAttribute(metadata); 123 if (Slogf.isLoggable(TAG, Log.DEBUG)) { 124 Slogf.d(TAG, "Requesting focus gain %d with attributes %s and zoneId %d", 125 focusGain, attributes, zoneId); 126 } 127 AudioAttributesWrapper audioAttributesWrapper = 128 mCarAudioContext.getAudioAttributeWrapperFromAttributes(attributes); 129 HalAudioFocusRequest currentRequest = 130 mHalFocusRequestsByZoneAndAttributes.get(zoneId).get(audioAttributesWrapper); 131 if (currentRequest != null) { 132 if (Slogf.isLoggable(TAG, Log.DEBUG)) { 133 Slogf.d(TAG, "A request already exists for zoneId %d and attributes %s", 134 zoneId, attributes); 135 } 136 mAudioControlWrapper.onAudioFocusChange(metadata, zoneId, 137 currentRequest.mFocusStatus); 138 } else { 139 makeAudioFocusRequestLocked(audioAttributesWrapper, zoneId, focusGain); 140 } 141 } 142 } 143 144 /** 145 * See {@link HalFocusListener#abandonAudioFocus(PlaybackTrackMetadata, int)} 146 */ abandonAudioFocus(PlaybackTrackMetadata metadata, int zoneId)147 public void abandonAudioFocus(PlaybackTrackMetadata metadata, int zoneId) { 148 synchronized (mLock) { 149 AudioAttributes attributes = metadataToAudioAttribute(metadata); 150 Preconditions.checkArgument(mHalFocusRequestsByZoneAndAttributes.contains(zoneId), 151 "Invalid zoneId %d provided in abandonAudioFocus", zoneId); 152 if (Slogf.isLoggable(TAG, Log.DEBUG)) { 153 Slogf.d(TAG, "Abandoning focus with attributes %s for zoneId %d", attributes, 154 zoneId); 155 } 156 abandonAudioFocusLocked( 157 mCarAudioContext.getAudioAttributeWrapperFromAttributes(attributes), zoneId); 158 } 159 } 160 161 /** 162 * Clear out all existing focus requests. Called when HAL dies. 163 */ reset()164 public void reset() { 165 Slogf.d(TAG, "Resetting HAL Audio Focus requests"); 166 synchronized (mLock) { 167 for (int i = 0; i < mHalFocusRequestsByZoneAndAttributes.size(); i++) { 168 int zoneId = mHalFocusRequestsByZoneAndAttributes.keyAt(i); 169 Map<AudioAttributesWrapper, HalAudioFocusRequest> 170 requestsByAttributes = mHalFocusRequestsByZoneAndAttributes.valueAt(i); 171 Set<AudioAttributesWrapper> wrapperSet = 172 new ArraySet<>(requestsByAttributes.keySet()); 173 for (AudioAttributesWrapper wrapper : wrapperSet) { 174 abandonAudioFocusLocked(wrapper, zoneId); 175 } 176 } 177 } 178 } 179 180 /** 181 * Returns the currently active {@link AudioAttributes}' for an audio zone 182 */ getActiveAudioAttributesForZone(int audioZoneId)183 public List<AudioAttributes> getActiveAudioAttributesForZone(int audioZoneId) { 184 synchronized (mLock) { 185 Map<AudioAttributesWrapper, HalAudioFocusRequest> halFocusRequestsForZone = 186 mHalFocusRequestsByZoneAndAttributes.get(audioZoneId); 187 List<AudioAttributes> activeAudioAttributes = 188 new ArrayList<>(halFocusRequestsForZone.size()); 189 190 for (AudioAttributesWrapper wrapper : halFocusRequestsForZone.keySet()) { 191 activeAudioAttributes.add(wrapper.getAudioAttributes()); 192 } 193 194 return activeAudioAttributes; 195 } 196 } 197 198 /** 199 * dumps the current state of the HalAudioFocus 200 * 201 * @param writer stream to write current state 202 */ 203 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)204 public void dump(IndentingPrintWriter writer) { 205 writer.println("*HalAudioFocus*"); 206 207 writer.increaseIndent(); 208 writer.println("Current focus requests:"); 209 writer.increaseIndent(); 210 synchronized (mLock) { 211 for (int i = 0; i < mHalFocusRequestsByZoneAndAttributes.size(); i++) { 212 int zoneId = mHalFocusRequestsByZoneAndAttributes.keyAt(i); 213 writer.printf("Zone %s:\n", zoneId); 214 writer.increaseIndent(); 215 216 Map<AudioAttributesWrapper, HalAudioFocusRequest> requestsByAttributes = 217 mHalFocusRequestsByZoneAndAttributes.valueAt(i); 218 for (HalAudioFocusRequest request : requestsByAttributes.values()) { 219 writer.printf("%s\n", request); 220 } 221 writer.decreaseIndent(); 222 } 223 } 224 writer.decreaseIndent(); 225 writer.decreaseIndent(); 226 } 227 228 /** 229 * dumps proto of the current state of the HalAudioFocus 230 * 231 * @param proto proto stream to write current state 232 */ 233 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dumpProto(ProtoOutputStream proto)234 public void dumpProto(ProtoOutputStream proto) { 235 long halAudioFocusToken = proto.start(CarAudioDumpProto.HAL_AUDIO_FOCUS); 236 synchronized (mLock) { 237 for (int i = 0; i < mHalFocusRequestsByZoneAndAttributes.size(); i++) { 238 int zoneId = mHalFocusRequestsByZoneAndAttributes.keyAt(i); 239 long halFocusRequestToken = proto.start(HAL_FOCUS_REQUESTS_BY_ZONE_AND_ATTRIBUTES); 240 proto.write(ZONE_ID, zoneId); 241 Map<AudioAttributesWrapper, HalAudioFocusRequest> requestsByAttributes = 242 mHalFocusRequestsByZoneAndAttributes.valueAt(i); 243 for (HalAudioFocusRequest request : requestsByAttributes.values()) { 244 proto.write(HAL_FOCUS_REQUESTS_BY_ATTRIBUTES, request.toString()); 245 } 246 proto.end(halFocusRequestToken); 247 } 248 } 249 proto.end(halAudioFocusToken); 250 } 251 252 @GuardedBy("mLock") abandonAudioFocusLocked(AudioAttributesWrapper audioAttributesWrapper, int zoneId)253 private void abandonAudioFocusLocked(AudioAttributesWrapper audioAttributesWrapper, 254 int zoneId) { 255 Map<AudioAttributesWrapper, HalAudioFocusRequest> halAudioFocusRequests = 256 mHalFocusRequestsByZoneAndAttributes.get(zoneId); 257 HalAudioFocusRequest currentRequest = halAudioFocusRequests.get(audioAttributesWrapper); 258 259 if (currentRequest == null) { 260 if (Slogf.isLoggable(TAG, Log.DEBUG)) { 261 Slogf.d(TAG, "No focus to abandon for audio attributes " + audioAttributesWrapper 262 + " and zoneId " + zoneId); 263 } 264 return; 265 } else { 266 // remove it from map 267 halAudioFocusRequests.remove(audioAttributesWrapper); 268 } 269 270 int result = mAudioManager.abandonAudioFocusRequest(currentRequest.mAudioFocusRequest); 271 if (result == AUDIOFOCUS_REQUEST_GRANTED) { 272 if (Slogf.isLoggable(TAG, Log.DEBUG)) { 273 Slogf.d(TAG, "Abandoned focus for audio attributes " + audioAttributesWrapper 274 + "and zoneId " + zoneId); 275 } 276 PlaybackTrackMetadata metadata = 277 audioAttributesWrapperToMetadata(audioAttributesWrapper); 278 mAudioControlWrapper.onAudioFocusChange(metadata, zoneId, AUDIOFOCUS_LOSS); 279 } else { 280 Slogf.w(TAG, "Failed to abandon focus for audio attributes " + audioAttributesWrapper 281 + " and zoneId " + zoneId); 282 } 283 } 284 generateAudioAttributes( AudioAttributesWrapper audioAttributesWrapper, int zoneId)285 private AudioAttributes generateAudioAttributes( 286 AudioAttributesWrapper audioAttributesWrapper, int zoneId) { 287 AudioAttributes.Builder builder = 288 new AudioAttributes.Builder(audioAttributesWrapper.getAudioAttributes()); 289 Bundle bundle = new Bundle(); 290 bundle.putInt(CarAudioManager.AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID, zoneId); 291 builder.addBundle(bundle); 292 293 return builder.build(); 294 } 295 296 @GuardedBy("mLock") generateFocusRequestLocked( AudioAttributesWrapper audioAttributesWrapper, int zoneId, int focusGain)297 private AudioFocusRequest generateFocusRequestLocked( 298 AudioAttributesWrapper audioAttributesWrapper, 299 int zoneId, int focusGain) { 300 AudioAttributes attributes = generateAudioAttributes(audioAttributesWrapper, zoneId); 301 return new AudioFocusRequest.Builder(focusGain) 302 .setAudioAttributes(attributes) 303 .setOnAudioFocusChangeListener((int focusChange) -> { 304 onAudioFocusChange(attributes, zoneId, focusChange); 305 }) 306 .build(); 307 } 308 onAudioFocusChange(AudioAttributes attributes, int zoneId, int focusChange)309 private void onAudioFocusChange(AudioAttributes attributes, int zoneId, int focusChange) { 310 AudioAttributesWrapper wrapper = 311 mCarAudioContext.getAudioAttributeWrapperFromAttributes(attributes); 312 synchronized (mLock) { 313 HalAudioFocusRequest currentRequest = 314 mHalFocusRequestsByZoneAndAttributes.get(zoneId).get(wrapper); 315 if (currentRequest != null) { 316 if (focusChange == AUDIOFOCUS_LOSS) { 317 mHalFocusRequestsByZoneAndAttributes.get(zoneId).remove(wrapper); 318 } else { 319 currentRequest.mFocusStatus = focusChange; 320 } 321 PlaybackTrackMetadata metadata = audioAttributesWrapperToMetadata(wrapper); 322 mAudioControlWrapper.onAudioFocusChange(metadata, zoneId, focusChange); 323 } 324 325 } 326 } 327 328 @GuardedBy("mLock") makeAudioFocusRequestLocked( AudioAttributesWrapper audioAttributesWrapper, int zoneId, int focusGain)329 private void makeAudioFocusRequestLocked( 330 AudioAttributesWrapper audioAttributesWrapper, 331 int zoneId, int focusGain) { 332 AudioFocusRequest audioFocusRequest = 333 generateFocusRequestLocked(audioAttributesWrapper, zoneId, focusGain); 334 335 int requestResult = mAudioManager.requestAudioFocus(audioFocusRequest); 336 337 int resultingFocusGain = focusGain; 338 339 if (requestResult == AUDIOFOCUS_REQUEST_GRANTED) { 340 HalAudioFocusRequest halAudioFocusRequest = 341 new HalAudioFocusRequest(audioFocusRequest, focusGain); 342 mHalFocusRequestsByZoneAndAttributes.get(zoneId) 343 .put(audioAttributesWrapper, halAudioFocusRequest); 344 handleNewlyActiveHalPlayback(audioAttributesWrapper.getAudioAttributes(), zoneId); 345 } else if (requestResult == AUDIOFOCUS_REQUEST_FAILED) { 346 resultingFocusGain = AUDIOFOCUS_LOSS; 347 } else if (requestResult == AUDIOFOCUS_REQUEST_DELAYED) { 348 // Delayed audio focus is not supported from HAL audio focus 349 Slogf.w(TAG, "Delayed result for request with audio attributes " 350 + audioAttributesWrapper + ", zoneId " + zoneId 351 + ", and focusGain " + focusGain); 352 resultingFocusGain = AUDIOFOCUS_LOSS; 353 } 354 PlaybackTrackMetadata metadata = audioAttributesWrapperToMetadata(audioAttributesWrapper); 355 mAudioControlWrapper.onAudioFocusChange(metadata, zoneId, resultingFocusGain); 356 } 357 handleNewlyActiveHalPlayback(AudioAttributes attributes, int zoneId)358 private void handleNewlyActiveHalPlayback(AudioAttributes attributes, int zoneId) { 359 if (mCarAudioPlaybackMonitor == null) { 360 return; 361 } 362 long identity = Binder.clearCallingIdentity(); 363 try { 364 mCarAudioPlaybackMonitor.onActiveAudioPlaybackAttributesAdded(List.of( 365 new Pair<>(attributes, Binder.getCallingUid())), zoneId); 366 } finally { 367 Binder.restoreCallingIdentity(identity); 368 } 369 } 370 371 private static final class HalAudioFocusRequest { 372 final AudioFocusRequest mAudioFocusRequest; 373 374 int mFocusStatus; 375 HalAudioFocusRequest(AudioFocusRequest audioFocusRequest, int focusStatus)376 HalAudioFocusRequest(AudioFocusRequest audioFocusRequest, int focusStatus) { 377 mAudioFocusRequest = audioFocusRequest; 378 mFocusStatus = focusStatus; 379 } 380 381 @Override 382 @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) toString()383 public String toString() { 384 return new StringBuilder() 385 .append("Request: ") 386 .append("[Audio attributes: ") 387 .append(mAudioFocusRequest.getAudioAttributes()) 388 .append(", Focus request: ") 389 .append(mAudioFocusRequest.getFocusGain()) 390 .append("]") 391 .append(", Status: ") 392 .append(mFocusStatus) 393 .toString(); 394 } 395 } 396 } 397