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.media.AudioManager.AUDIOFOCUS_REQUEST_FAILED; 20 21 import static com.android.car.audio.FocusInteraction.AUDIO_FOCUS_NAVIGATION_REJECTED_DURING_CALL_URI; 22 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 23 24 import static java.util.Collections.EMPTY_LIST; 25 26 import android.annotation.NonNull; 27 import android.annotation.Nullable; 28 import android.annotation.UserIdInt; 29 import android.car.builtin.util.Slogf; 30 import android.car.media.CarAudioManager; 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) { 285 int bundleZoneId = 286 bundle.getInt(CarAudioManager.AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID, 287 -1); 288 // check if the zone id is within current zones bounds 289 if (service.isAudioZoneIdValid(bundleZoneId)) { 290 Slogf.d(TAG, "getFocusForAudioFocusInfo valid zoneId %d with bundle request for" 291 + " client %s", bundleZoneId, afi.getClientId()); 292 zoneId = bundleZoneId; 293 } else { 294 Slogf.w(TAG, "getFocusForAudioFocusInfo invalid zoneId %d with bundle request for " 295 + "client %s, dispatching focus request to zoneId %d", bundleZoneId, 296 afi.getClientId(), zoneId); 297 } 298 } 299 300 return zoneId; 301 } 302 notifyFocusListeners(int[] zoneIds)303 private void notifyFocusListeners(int[] zoneIds) { 304 SparseArray<List<AudioFocusInfo>> focusHoldersByZoneId = new SparseArray<>(zoneIds.length); 305 for (int i = 0; i < zoneIds.length; i++) { 306 int zoneId = zoneIds[i]; 307 focusHoldersByZoneId.put(zoneId, mFocusZones.get(zoneId).getAudioFocusHolders()); 308 sendFocusChangeToOemService(getCarAudioFocusForZoneId(zoneId), zoneId); 309 } 310 311 if (mCarFocusCallback == null) { 312 return; 313 } 314 mCarFocusCallback.onFocusChange(zoneIds, focusHoldersByZoneId); 315 } 316 sendFocusChangeToOemService(CarAudioFocus carAudioFocus, int zoneId)317 private void sendFocusChangeToOemService(CarAudioFocus carAudioFocus, int zoneId) { 318 CarOemProxyService proxy = CarLocalServices.getService(CarOemProxyService.class); 319 if (!proxy.isOemServiceEnabled() || !proxy.isOemServiceReady() 320 || proxy.getCarOemAudioFocusService() == null) { 321 return; 322 } 323 324 proxy.getCarOemAudioFocusService().notifyAudioFocusChange( 325 carAudioFocus.getAudioFocusHolders(), carAudioFocus.getAudioFocusLosers(), zoneId); 326 } 327 328 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)329 void dump(IndentingPrintWriter writer) { 330 writer.println("*CarZonesAudioFocus*"); 331 writer.increaseIndent(); 332 writer.printf("Has Focus Callback: %b\n", mCarFocusCallback != null); 333 writer.println("Car Zones Audio Focus Listeners:"); 334 writer.increaseIndent(); 335 for (int i = 0; i < mFocusZones.size(); i++) { 336 writer.printf("Zone Id: %d\n", mFocusZones.keyAt(i)); 337 writer.increaseIndent(); 338 mFocusZones.valueAt(i).dump(writer); 339 writer.decreaseIndent(); 340 } 341 writer.decreaseIndent(); 342 writer.decreaseIndent(); 343 } 344 345 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dumpProto(ProtoOutputStream proto)346 public void dumpProto(ProtoOutputStream proto) { 347 long focusHandlerToken = proto.start(CarAudioDumpProto.FOCUS_HANDLER); 348 proto.write(CarAudioZoneFocusProto.HAS_FOCUS_CALLBACK, mCarFocusCallback != null); 349 for (int i = 0; i < mFocusZones.size(); i++) { 350 mFocusZones.valueAt(i).dumpProto(proto); 351 } 352 proto.end(focusHandlerToken); 353 } 354 updateUserForZoneId(int audioZoneId, @UserIdInt int userId)355 public void updateUserForZoneId(int audioZoneId, @UserIdInt int userId) { 356 synchronized (mLock) { 357 Preconditions.checkArgument(mCarAudioService.isAudioZoneIdValid(audioZoneId), 358 "Invalid zoneId %d", audioZoneId); 359 } 360 mFocusZones.get(audioZoneId).getFocusInteraction().setUserIdForSettings(userId); 361 } 362 transientlyLoseMediaAudioFocusForUser(int userId, int zoneId)363 AudioFocusStack transientlyLoseMediaAudioFocusForUser(int userId, int zoneId) { 364 AudioAttributes audioAttributes = 365 new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(); 366 CarAudioFocus carAudioFocus = mFocusZones.get(zoneId); 367 List<AudioFocusInfo> activeFocusInfos = carAudioFocus 368 .getActiveAudioFocusForUserAndAudioAttributes(audioAttributes, userId); 369 List<AudioFocusInfo> inactiveFocusInfos = carAudioFocus 370 .getInactiveAudioFocusForUserAndAudioAttributes(audioAttributes, userId); 371 372 return transientlyLoserFocusForFocusStack(carAudioFocus, activeFocusInfos, 373 inactiveFocusInfos); 374 } 375 transientlyLoseAudioFocusForZone(int zoneId)376 AudioFocusStack transientlyLoseAudioFocusForZone(int zoneId) { 377 CarAudioFocus carAudioFocus = mFocusZones.get(zoneId); 378 List<AudioFocusInfo> activeFocusInfos = carAudioFocus.getAudioFocusHolders(); 379 List<AudioFocusInfo> inactiveFocusInfos = carAudioFocus.getAudioFocusLosers(); 380 381 return transientlyLoserFocusForFocusStack(carAudioFocus, activeFocusInfos, 382 inactiveFocusInfos); 383 } 384 transientlyLoserFocusForFocusStack(CarAudioFocus carAudioFocus, List<AudioFocusInfo> activeFocusInfos, List<AudioFocusInfo> inactiveFocusInfos)385 private AudioFocusStack transientlyLoserFocusForFocusStack(CarAudioFocus carAudioFocus, 386 List<AudioFocusInfo> activeFocusInfos, List<AudioFocusInfo> inactiveFocusInfos) { 387 // Order matters here: Remove the focus losers first 388 // then do the current holder to prevent loser from popping up while 389 // the focus is being removed for current holders 390 // Remove focus for current focus losers 391 if (!inactiveFocusInfos.isEmpty()) { 392 transientlyLoseInFocusInZone(inactiveFocusInfos, carAudioFocus); 393 } 394 395 if (!activeFocusInfos.isEmpty()) { 396 transientlyLoseInFocusInZone(activeFocusInfos, carAudioFocus); 397 } 398 399 return new AudioFocusStack(activeFocusInfos, inactiveFocusInfos); 400 } 401 regainMediaAudioFocusInZone(AudioFocusStack mediaFocusStack, int zoneId)402 void regainMediaAudioFocusInZone(AudioFocusStack mediaFocusStack, int zoneId) { 403 CarAudioFocus carAudioFocus = mFocusZones.get(zoneId); 404 405 // Order matters here: Regain focus for 406 // previously lost focus holders then regain 407 // focus for holders that had it last. 408 // Regain focus for the focus losers from previous zone. 409 if (!mediaFocusStack.getInactiveFocusList().isEmpty()) { 410 regainMediaAudioFocusInZone(mediaFocusStack.getInactiveFocusList(), carAudioFocus); 411 } 412 413 if (!mediaFocusStack.getActiveFocusList().isEmpty()) { 414 regainMediaAudioFocusInZone(mediaFocusStack.getActiveFocusList(), carAudioFocus); 415 } 416 } 417 regainMediaAudioFocusInZone(List<AudioFocusInfo> focusInfos, CarAudioFocus carAudioFocus)418 private void regainMediaAudioFocusInZone(List<AudioFocusInfo> focusInfos, 419 CarAudioFocus carAudioFocus) { 420 for (int index = 0; index < focusInfos.size(); index++) { 421 carAudioFocus.reevaluateAndRegainAudioFocus(focusInfos.get(index)); 422 } 423 } 424 425 /** 426 * Callback to get notified of the active focus holders after any focus request or abandon call 427 */ 428 public interface CarFocusCallback { 429 /** 430 * Called after a focus request or abandon call is handled. 431 * 432 * @param audioZoneIds IDs of the zones where the changes took place 433 * @param focusHoldersByZoneId sparse array by zone ID, where each value is a list of 434 * {@link AudioFocusInfo}s holding focus in specified audio zone 435 */ onFocusChange(int[] audioZoneIds, @NonNull SparseArray<List<AudioFocusInfo>> focusHoldersByZoneId)436 void onFocusChange(int[] audioZoneIds, 437 @NonNull SparseArray<List<AudioFocusInfo>> focusHoldersByZoneId); 438 } 439 } 440