• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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