• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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;
18 
19 import static android.car.media.CarAudioManager.AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID;
20 import static android.media.AudioManager.AUDIOFOCUS_REQUEST_FAILED;
21 
22 import static com.android.car.audio.FocusInteraction.AUDIO_FOCUS_NAVIGATION_REJECTED_DURING_CALL_URI;
23 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
24 
25 import static java.util.Collections.EMPTY_LIST;
26 
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.annotation.UserIdInt;
30 import android.car.builtin.util.Slogf;
31 import android.car.oem.CarAudioFeaturesInfo;
32 import android.content.pm.PackageManager;
33 import android.media.AudioAttributes;
34 import android.media.AudioFocusInfo;
35 import android.media.AudioManager;
36 import android.media.audiopolicy.AudioPolicy;
37 import android.os.Bundle;
38 import android.util.ArraySet;
39 import android.util.SparseArray;
40 import android.util.proto.ProtoOutputStream;
41 
42 import com.android.car.CarLocalServices;
43 import com.android.car.CarLog;
44 import com.android.car.audio.CarAudioDumpProto.CarAudioZoneFocusProto;
45 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
46 import com.android.car.internal.util.IndentingPrintWriter;
47 import com.android.car.oem.CarOemProxyService;
48 import com.android.internal.annotations.GuardedBy;
49 import com.android.internal.annotations.VisibleForTesting;
50 import com.android.internal.util.Preconditions;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 import java.util.Objects;
55 
56 /**
57  * Implements {@link AudioPolicy.AudioPolicyFocusListener}
58  *
59  * <p><b>Note:</b> Manages audio focus on a per zone basis.
60  */
61 final class CarZonesAudioFocus extends AudioPolicy.AudioPolicyFocusListener {
62     private static final String TAG = CarLog.tagFor(CarZonesAudioFocus.class);
63 
64     private final CarFocusCallback mCarFocusCallback;
65     private final Object mLock = new Object();
66     @GuardedBy("mLock")
67     private CarAudioService mCarAudioService; // Dynamically assigned just after construction
68     @GuardedBy("mLock")
69     private AudioPolicy mAudioPolicy; // Dynamically assigned just after construction
70 
71     private final SparseArray<CarAudioFocus> mFocusZones;
72 
createCarZonesAudioFocus(AudioManagerWrapper audioManager, PackageManager packageManager, SparseArray<CarAudioZone> carAudioZones, CarAudioSettings carAudioSettings, CarFocusCallback carFocusCallback, CarVolumeInfoWrapper carVolumeInfoWrapper, @Nullable CarAudioFeaturesInfo features)73     public static CarZonesAudioFocus createCarZonesAudioFocus(AudioManagerWrapper audioManager,
74             PackageManager packageManager, SparseArray<CarAudioZone> carAudioZones,
75             CarAudioSettings carAudioSettings, CarFocusCallback carFocusCallback,
76             CarVolumeInfoWrapper carVolumeInfoWrapper, @Nullable CarAudioFeaturesInfo features) {
77         Objects.requireNonNull(audioManager, "Audio manager cannot be null");
78         Objects.requireNonNull(packageManager, "Package manager cannot be null");
79         Objects.requireNonNull(carAudioZones, "Car audio zones cannot be null");
80         Preconditions.checkArgument(carAudioZones.size() != 0,
81                 "There must be a minimum of one audio zone");
82         Objects.requireNonNull(carAudioSettings, "Car audio settings cannot be null");
83         Objects.requireNonNull(carVolumeInfoWrapper, "Car volume info cannot be null");
84 
85         SparseArray<CarAudioFocus> audioFocusPerZone = new SparseArray<>();
86 
87         ContentObserverFactory observerFactory = new ContentObserverFactory(
88                 AUDIO_FOCUS_NAVIGATION_REJECTED_DURING_CALL_URI);
89         //Create focus for all the zones
90         for (int i = 0; i < carAudioZones.size(); i++) {
91             CarAudioZone audioZone = carAudioZones.valueAt(i);
92             int audioZoneId = audioZone.getId();
93             Slogf.d(TAG, "Adding new zone %d", audioZoneId);
94             FocusInteraction interaction = new FocusInteraction(carAudioSettings, observerFactory);
95             CarAudioFocus zoneFocusListener = new CarAudioFocus(audioManager, packageManager,
96                     interaction, audioZone, carVolumeInfoWrapper, features);
97             audioFocusPerZone.put(audioZoneId, zoneFocusListener);
98         }
99         return new CarZonesAudioFocus(audioFocusPerZone, carFocusCallback);
100     }
101 
102     @VisibleForTesting
CarZonesAudioFocus(SparseArray<CarAudioFocus> focusZones, CarFocusCallback carFocusCallback)103     CarZonesAudioFocus(SparseArray<CarAudioFocus> focusZones, CarFocusCallback carFocusCallback) {
104         mFocusZones = focusZones;
105         mCarFocusCallback = carFocusCallback;
106     }
107 
108     /**
109      * Query the current list of focus loser in zoneId for uid
110      * @param uid uid to query for current focus losers
111      * @param zoneId zone id to query for info
112      * @return list of current focus losers for uid
113      */
getAudioFocusLosersForUid(int uid, int zoneId)114     ArrayList<AudioFocusInfo> getAudioFocusLosersForUid(int uid, int zoneId) {
115         CarAudioFocus focus = mFocusZones.get(zoneId);
116         return focus.getAudioFocusLosersForUid(uid);
117     }
118 
119     /**
120      * Query the current list of focus holders in zoneId for uid
121      * @param uid uid to query for current focus holders
122      * @param zoneId zone id to query
123      * @return list of current focus holders that for uid
124      */
getAudioFocusHoldersForUid(int uid, int zoneId)125     ArrayList<AudioFocusInfo> getAudioFocusHoldersForUid(int uid, int zoneId) {
126         CarAudioFocus focus = mFocusZones.get(zoneId);
127         return focus.getAudioFocusHoldersForUid(uid);
128     }
129 
130 
131     /**
132      * For the zone queried, transiently lose all active focus entries
133      * @param zoneId zone id where all focus entries should be lost
134      * @return focus entries lost in the zone.
135      */
transientlyLoseAllFocusHoldersInZone(int zoneId)136     List<AudioFocusInfo> transientlyLoseAllFocusHoldersInZone(int zoneId) {
137         CarAudioFocus focus = mFocusZones.get(zoneId);
138         List<AudioFocusInfo> activeFocusInfos = focus.getAudioFocusHolders();
139         if (!activeFocusInfos.isEmpty()) {
140             transientlyLoseInFocusInZone(activeFocusInfos, zoneId);
141         }
142         return activeFocusInfos;
143     }
144 
145     /**
146      * For each entry in list, transiently lose focus
147      * @param afiList list of audio focus entries
148      * @param zoneId zone id where focus should be lost
149      */
transientlyLoseInFocusInZone(List<AudioFocusInfo> afiList, int zoneId)150     void transientlyLoseInFocusInZone(List<AudioFocusInfo> afiList, int zoneId) {
151         CarAudioFocus focus = mFocusZones.get(zoneId);
152 
153         transientlyLoseInFocusInZone(afiList, focus);
154     }
155 
transientlyLoseInFocusInZone(List<AudioFocusInfo> audioFocusInfos, CarAudioFocus focus)156     private void transientlyLoseInFocusInZone(List<AudioFocusInfo> audioFocusInfos,
157             CarAudioFocus focus) {
158         for (int index = 0; index < audioFocusInfos.size(); index++) {
159             AudioFocusInfo info = audioFocusInfos.get(index);
160             focus.removeAudioFocusInfoAndTransientlyLoseFocus(info);
161         }
162     }
163 
164     /**
165      * For each entry in list, reevaluate and regain focus and notify focus listener of its zone
166      *
167      * @param audioFocusInfos list of audio focus entries to reevaluate and regain
168      * @return list of results for regaining focus
169      */
reevaluateAndRegainAudioFocusList(List<AudioFocusInfo> audioFocusInfos)170     List<Integer> reevaluateAndRegainAudioFocusList(List<AudioFocusInfo> audioFocusInfos) {
171         CarAudioService service;
172         synchronized (mLock) {
173             service = mCarAudioService;
174         }
175         if (service == null) {
176             Slogf.e(TAG, "reevaluateAndRegainAudioFocusList failed,"
177                     + " car audio service unavailable");
178             return EMPTY_LIST;
179         }
180         List<Integer> res = new ArrayList<>(audioFocusInfos.size());
181         ArraySet<Integer> zoneIds = new ArraySet<>();
182         for (int index = 0; index < audioFocusInfos.size(); index++) {
183             AudioFocusInfo afi = audioFocusInfos.get(index);
184             int zoneId = getAudioZoneIdForAudioFocusInfo(afi, service);
185             res.add(getCarAudioFocusForZoneId(zoneId).reevaluateAndRegainAudioFocus(afi));
186             zoneIds.add(zoneId);
187         }
188         int[] zoneIdArray = new int[zoneIds.size()];
189         for (int zoneIdIndex = 0; zoneIdIndex < zoneIds.size(); zoneIdIndex++) {
190             zoneIdArray[zoneIdIndex] = zoneIds.valueAt(zoneIdIndex);
191         }
192         notifyFocusListeners(zoneIdArray);
193         return res;
194     }
195 
reevaluateAndRegainAudioFocus(AudioFocusInfo afi)196     int reevaluateAndRegainAudioFocus(AudioFocusInfo afi) {
197         CarAudioService service;
198         synchronized (mLock) {
199             service = mCarAudioService;
200         }
201         if (service == null) {
202             Slogf.e(TAG, "reevaluateAndRegainAudioFocus failed, car audio service unavailable");
203             return AUDIOFOCUS_REQUEST_FAILED;
204         }
205         int zoneId = getAudioZoneIdForAudioFocusInfo(afi, service);
206         return getCarAudioFocusForZoneId(zoneId).reevaluateAndRegainAudioFocus(afi);
207     }
208 
209     /**
210      * Sets the owning policy of the audio focus
211      *
212      * <p><b>Note:</b> This has to happen after the construction to avoid a chicken and egg
213      * problem when setting up the AudioPolicy which must depend on this object.
214 
215      * @param carAudioService owning car audio service
216      * @param parentPolicy owning parent car audio policy
217      */
setOwningPolicy(CarAudioService carAudioService, AudioPolicy parentPolicy)218     void setOwningPolicy(CarAudioService carAudioService, AudioPolicy parentPolicy) {
219         synchronized (mLock) {
220             mAudioPolicy = parentPolicy;
221             mCarAudioService = carAudioService;
222         }
223 
224         for (int i = 0; i < mFocusZones.size(); i++) {
225             mFocusZones.valueAt(i).setOwningPolicy(parentPolicy);
226         }
227     }
228 
setRestrictFocus(boolean isFocusRestricted)229     void setRestrictFocus(boolean isFocusRestricted) {
230         int[] zoneIds = new int[mFocusZones.size()];
231         for (int i = 0; i < mFocusZones.size(); i++) {
232             zoneIds[i] = mFocusZones.keyAt(i);
233             mFocusZones.valueAt(i).setRestrictFocus(isFocusRestricted);
234         }
235         notifyFocusListeners(zoneIds);
236     }
237 
238     @Override
onAudioFocusRequest(AudioFocusInfo afi, int requestResult)239     public void onAudioFocusRequest(AudioFocusInfo afi, int requestResult) {
240         CarAudioService service;
241         synchronized (mLock) {
242             service = mCarAudioService;
243         }
244         if (service == null) {
245             Slogf.e(TAG, "onAudioFocusRequest failed, car audio service unavailable");
246             return;
247         }
248         int zoneId = getAudioZoneIdForAudioFocusInfo(afi, service);
249         getCarAudioFocusForZoneId(zoneId).onAudioFocusRequest(afi, requestResult);
250         notifyFocusListeners(new int[]{zoneId});
251     }
252 
253     /**
254      * @see AudioManager#abandonAudioFocus(AudioManager.OnAudioFocusChangeListener, AudioAttributes)
255      * Note that we'll get this call for a focus holder that dies while in the focus stack, so
256      * we don't need to watch for death notifications directly.
257      */
258     @Override
onAudioFocusAbandon(AudioFocusInfo afi)259     public void onAudioFocusAbandon(AudioFocusInfo afi) {
260         CarAudioService service;
261         synchronized (mLock) {
262             service = mCarAudioService;
263         }
264         if (service == null) {
265             Slogf.e(TAG, "onAudioFocusAbandon failed, car audio service unavailable");
266             return;
267         }
268         int zoneId = getAudioZoneIdForAudioFocusInfo(afi, service);
269         getCarAudioFocusForZoneId(zoneId).onAudioFocusAbandon(afi);
270         notifyFocusListeners(new int[]{zoneId});
271     }
272 
getCarAudioFocusForZoneId(int zoneId)273     private CarAudioFocus getCarAudioFocusForZoneId(int zoneId) {
274         return mFocusZones.get(zoneId);
275     }
276 
getAudioZoneIdForAudioFocusInfo(AudioFocusInfo afi, CarAudioService service)277     private int getAudioZoneIdForAudioFocusInfo(AudioFocusInfo afi, CarAudioService service) {
278         int zoneId = service.getZoneIdForAudioFocusInfo(afi);
279 
280         // If the bundle attribute for AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID has been assigned
281         // Use zone id from that instead.
282         Bundle bundle = afi.getAttributes().getBundle();
283 
284         if (bundle != null && bundle.containsKey(AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID)) {
285             int requestId = bundle.getInt(AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID, /* defaultValue= */-1);
286             // check if the zone id is within current zones bounds
287             if (service.isAudioZoneIdValid(requestId)) {
288                 Slogf.d(TAG, "getFocusForAudioFocusInfo valid zoneId %d with bundle request for"
289                         + " client %s", requestId, afi.getClientId());
290                 zoneId = requestId;
291             } else {
292                 Slogf.w(TAG, "getFocusForAudioFocusInfo invalid zoneId %d with bundle request for "
293                                 + "client %s, dispatching focus request to zoneId %d", requestId,
294                         afi.getClientId(), zoneId);
295             }
296         }
297 
298         return zoneId;
299     }
300 
notifyFocusListeners(int[] zoneIds)301     private void notifyFocusListeners(int[] zoneIds) {
302         SparseArray<List<AudioFocusInfo>> focusHoldersByZoneId = new SparseArray<>(zoneIds.length);
303         for (int i = 0; i < zoneIds.length; i++) {
304             int zoneId = zoneIds[i];
305             focusHoldersByZoneId.put(zoneId, mFocusZones.get(zoneId).getAudioFocusHolders());
306             sendFocusChangeToOemService(getCarAudioFocusForZoneId(zoneId), zoneId);
307         }
308 
309         if (mCarFocusCallback == null) {
310             return;
311         }
312         mCarFocusCallback.onFocusChange(zoneIds, focusHoldersByZoneId);
313     }
314 
sendFocusChangeToOemService(CarAudioFocus carAudioFocus, int zoneId)315     private void sendFocusChangeToOemService(CarAudioFocus carAudioFocus, int zoneId) {
316         CarOemProxyService proxy = CarLocalServices.getService(CarOemProxyService.class);
317         if (!proxy.isOemServiceEnabled() || !proxy.isOemServiceReady()
318                 || proxy.getCarOemAudioFocusService() == null) {
319             return;
320         }
321 
322         proxy.getCarOemAudioFocusService().notifyAudioFocusChange(
323                 carAudioFocus.getAudioFocusHolders(), carAudioFocus.getAudioFocusLosers(), zoneId);
324     }
325 
326     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)327     void dump(IndentingPrintWriter writer) {
328         writer.println("*CarZonesAudioFocus*");
329         writer.increaseIndent();
330         writer.printf("Has Focus Callback: %b\n", mCarFocusCallback != null);
331         writer.println("Car Zones Audio Focus Listeners:");
332         writer.increaseIndent();
333         for (int i = 0; i < mFocusZones.size(); i++) {
334             writer.printf("Zone Id: %d\n", mFocusZones.keyAt(i));
335             writer.increaseIndent();
336             mFocusZones.valueAt(i).dump(writer);
337             writer.decreaseIndent();
338         }
339         writer.decreaseIndent();
340         writer.decreaseIndent();
341     }
342 
343     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProto(ProtoOutputStream proto)344     public void dumpProto(ProtoOutputStream proto) {
345         long focusHandlerToken = proto.start(CarAudioDumpProto.FOCUS_HANDLER);
346         proto.write(CarAudioZoneFocusProto.HAS_FOCUS_CALLBACK, mCarFocusCallback != null);
347         for (int i = 0; i < mFocusZones.size(); i++) {
348             mFocusZones.valueAt(i).dumpProto(proto);
349         }
350         proto.end(focusHandlerToken);
351     }
352 
updateUserForZoneId(int audioZoneId, @UserIdInt int userId)353     public void updateUserForZoneId(int audioZoneId, @UserIdInt int userId) {
354         synchronized (mLock) {
355             Preconditions.checkArgument(mCarAudioService.isAudioZoneIdValid(audioZoneId),
356                     "Invalid zoneId %d", audioZoneId);
357         }
358         mFocusZones.get(audioZoneId).getFocusInteraction().setUserIdForSettings(userId);
359     }
360 
transientlyLoseMediaAudioFocusForUser(int userId, int zoneId)361     AudioFocusStack transientlyLoseMediaAudioFocusForUser(int userId, int zoneId) {
362         AudioAttributes audioAttributes =
363                 new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
364         CarAudioFocus carAudioFocus = mFocusZones.get(zoneId);
365         List<AudioFocusInfo> activeFocusInfos = carAudioFocus
366                 .getActiveAudioFocusForUserAndAudioAttributes(audioAttributes, userId);
367         List<AudioFocusInfo> inactiveFocusInfos = carAudioFocus
368                 .getInactiveAudioFocusForUserAndAudioAttributes(audioAttributes, userId);
369 
370         return transientlyLoserFocusForFocusStack(carAudioFocus, activeFocusInfos,
371                 inactiveFocusInfos);
372     }
373 
transientlyLoseAudioFocusForZone(int zoneId)374     AudioFocusStack transientlyLoseAudioFocusForZone(int zoneId) {
375         CarAudioFocus carAudioFocus = mFocusZones.get(zoneId);
376         List<AudioFocusInfo> activeFocusInfos = carAudioFocus.getAudioFocusHolders();
377         List<AudioFocusInfo> inactiveFocusInfos = carAudioFocus.getAudioFocusLosers();
378 
379         return transientlyLoserFocusForFocusStack(carAudioFocus, activeFocusInfos,
380                 inactiveFocusInfos);
381     }
382 
transientlyLoserFocusForFocusStack(CarAudioFocus carAudioFocus, List<AudioFocusInfo> activeFocusInfos, List<AudioFocusInfo> inactiveFocusInfos)383     private AudioFocusStack transientlyLoserFocusForFocusStack(CarAudioFocus carAudioFocus,
384             List<AudioFocusInfo> activeFocusInfos, List<AudioFocusInfo> inactiveFocusInfos) {
385         // Order matters here: Remove the focus losers first
386         // then do the current holder to prevent loser from popping up while
387         // the focus is being removed for current holders
388         // Remove focus for current focus losers
389         if (!inactiveFocusInfos.isEmpty()) {
390             transientlyLoseInFocusInZone(inactiveFocusInfos, carAudioFocus);
391         }
392 
393         if (!activeFocusInfos.isEmpty()) {
394             transientlyLoseInFocusInZone(activeFocusInfos, carAudioFocus);
395         }
396 
397         return new AudioFocusStack(activeFocusInfos, inactiveFocusInfos);
398     }
399 
regainMediaAudioFocusInZone(AudioFocusStack mediaFocusStack, int zoneId)400     void regainMediaAudioFocusInZone(AudioFocusStack mediaFocusStack, int zoneId) {
401         CarAudioFocus carAudioFocus = mFocusZones.get(zoneId);
402 
403         // Order matters here: Regain focus for
404         // previously lost focus holders then regain
405         // focus for holders that had it last.
406         // Regain focus for the focus losers from previous zone.
407         if (!mediaFocusStack.getInactiveFocusList().isEmpty()) {
408             regainMediaAudioFocusInZone(mediaFocusStack.getInactiveFocusList(), carAudioFocus);
409         }
410 
411         if (!mediaFocusStack.getActiveFocusList().isEmpty()) {
412             regainMediaAudioFocusInZone(mediaFocusStack.getActiveFocusList(), carAudioFocus);
413         }
414     }
415 
regainMediaAudioFocusInZone(List<AudioFocusInfo> focusInfos, CarAudioFocus carAudioFocus)416     private void regainMediaAudioFocusInZone(List<AudioFocusInfo> focusInfos,
417             CarAudioFocus carAudioFocus) {
418         for (int index = 0; index < focusInfos.size(); index++) {
419             carAudioFocus.reevaluateAndRegainAudioFocus(focusInfos.get(index));
420         }
421     }
422 
423     /**
424      * Callback to get notified of the active focus holders after any focus request or abandon  call
425      */
426     public interface CarFocusCallback {
427         /**
428          * Called after a focus request or abandon call is handled.
429          *
430          * @param audioZoneIds IDs of the zones where the changes took place
431          * @param focusHoldersByZoneId sparse array by zone ID, where each value is a list of
432          * {@link AudioFocusInfo}s holding focus in specified audio zone
433          */
onFocusChange(int[] audioZoneIds, @NonNull SparseArray<List<AudioFocusInfo>> focusHoldersByZoneId)434         void onFocusChange(int[] audioZoneIds,
435                 @NonNull SparseArray<List<AudioFocusInfo>> focusHoldersByZoneId);
436     }
437 }
438