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 com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 20 21 import android.annotation.NonNull; 22 import android.annotation.UserIdInt; 23 import android.car.builtin.util.Slogf; 24 import android.car.media.CarAudioManager; 25 import android.content.pm.PackageManager; 26 import android.media.AudioAttributes; 27 import android.media.AudioFocusInfo; 28 import android.media.AudioManager; 29 import android.media.audiopolicy.AudioPolicy; 30 import android.os.Bundle; 31 import android.util.SparseArray; 32 33 import com.android.car.CarLocalServices; 34 import com.android.car.CarLog; 35 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 36 import com.android.car.internal.util.IndentingPrintWriter; 37 import com.android.car.oem.CarOemProxyService; 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.internal.util.Preconditions; 40 41 import java.util.ArrayList; 42 import java.util.List; 43 import java.util.Objects; 44 45 /** 46 * Implements {@link AudioPolicy.AudioPolicyFocusListener} 47 * 48 * <p><b>Note:</b> Manages audio focus on a per zone basis. 49 */ 50 final class CarZonesAudioFocus extends AudioPolicy.AudioPolicyFocusListener { 51 private static final String TAG = CarLog.tagFor(CarZonesAudioFocus.class); 52 53 private final CarFocusCallback mCarFocusCallback; 54 private CarAudioService mCarAudioService; // Dynamically assigned just after construction 55 private AudioPolicy mAudioPolicy; // Dynamically assigned just after construction 56 57 private final SparseArray<CarAudioFocus> mFocusZones; 58 createCarZonesAudioFocus(AudioManager audioManager, PackageManager packageManager, SparseArray<CarAudioZone> carAudioZones, CarAudioSettings carAudioSettings, CarFocusCallback carFocusCallback, CarVolumeInfoWrapper carVolumeInfoWrapper)59 public static CarZonesAudioFocus createCarZonesAudioFocus(AudioManager audioManager, 60 PackageManager packageManager, 61 SparseArray<CarAudioZone> carAudioZones, 62 CarAudioSettings carAudioSettings, 63 CarFocusCallback carFocusCallback, 64 CarVolumeInfoWrapper carVolumeInfoWrapper) { 65 Objects.requireNonNull(audioManager, "Audio manager cannot be null"); 66 Objects.requireNonNull(packageManager, "Package manager cannot be null"); 67 Objects.requireNonNull(carAudioZones, "Car audio zones cannot be null"); 68 Preconditions.checkArgument(carAudioZones.size() != 0, 69 "There must be a minimum of one audio zone"); 70 Objects.requireNonNull(carAudioSettings, "Car audio settings cannot be null"); 71 Objects.requireNonNull(carVolumeInfoWrapper, "Car volume info cannot be null"); 72 73 SparseArray<CarAudioFocus> audioFocusPerZone = new SparseArray<>(); 74 75 //Create focus for all the zones 76 for (int i = 0; i < carAudioZones.size(); i++) { 77 CarAudioZone audioZone = carAudioZones.valueAt(i); 78 int audioZoneId = audioZone.getId(); 79 Slogf.d(TAG, "Adding new zone %d", audioZoneId); 80 81 CarAudioFocus zoneFocusListener = new CarAudioFocus(audioManager, 82 packageManager, new FocusInteraction(carAudioSettings), 83 audioZone.getCarAudioContext(), carVolumeInfoWrapper, audioZoneId); 84 audioFocusPerZone.put(audioZoneId, zoneFocusListener); 85 } 86 return new CarZonesAudioFocus(audioFocusPerZone, carFocusCallback); 87 } 88 89 @VisibleForTesting CarZonesAudioFocus(SparseArray<CarAudioFocus> focusZones, CarFocusCallback carFocusCallback)90 CarZonesAudioFocus(SparseArray<CarAudioFocus> focusZones, CarFocusCallback carFocusCallback) { 91 mFocusZones = focusZones; 92 mCarFocusCallback = carFocusCallback; 93 } 94 95 /** 96 * Query the current list of focus loser in zoneId for uid 97 * @param uid uid to query for current focus losers 98 * @param zoneId zone id to query for info 99 * @return list of current focus losers for uid 100 */ getAudioFocusLosersForUid(int uid, int zoneId)101 ArrayList<AudioFocusInfo> getAudioFocusLosersForUid(int uid, int zoneId) { 102 CarAudioFocus focus = mFocusZones.get(zoneId); 103 return focus.getAudioFocusLosersForUid(uid); 104 } 105 106 /** 107 * Query the current list of focus holders in zoneId for uid 108 * @param uid uid to query for current focus holders 109 * @param zoneId zone id to query 110 * @return list of current focus holders that for uid 111 */ getAudioFocusHoldersForUid(int uid, int zoneId)112 ArrayList<AudioFocusInfo> getAudioFocusHoldersForUid(int uid, int zoneId) { 113 CarAudioFocus focus = mFocusZones.get(zoneId); 114 return focus.getAudioFocusHoldersForUid(uid); 115 } 116 117 /** 118 * For each entry in list, transiently lose focus 119 * @param afiList list of audio focus entries 120 * @param zoneId zone id where focus should should be lost 121 */ transientlyLoseInFocusInZone(@onNull ArrayList<AudioFocusInfo> afiList, int zoneId)122 void transientlyLoseInFocusInZone(@NonNull ArrayList<AudioFocusInfo> afiList, int zoneId) { 123 CarAudioFocus focus = mFocusZones.get(zoneId); 124 125 for (AudioFocusInfo info : afiList) { 126 focus.removeAudioFocusInfoAndTransientlyLoseFocus(info); 127 } 128 } 129 reevaluateAndRegainAudioFocus(AudioFocusInfo afi)130 int reevaluateAndRegainAudioFocus(AudioFocusInfo afi) { 131 int zoneId = getAudioZoneIdForAudioFocusInfo(afi); 132 return getCarAudioFocusForZoneId(zoneId).reevaluateAndRegainAudioFocus(afi); 133 } 134 135 /** 136 * Sets the owning policy of the audio focus 137 * 138 * <p><b>Note:</b> This has to happen after the construction to avoid a chicken and egg 139 * problem when setting up the AudioPolicy which must depend on this object. 140 141 * @param carAudioService owning car audio service 142 * @param parentPolicy owning parent car audio policy 143 */ setOwningPolicy(CarAudioService carAudioService, AudioPolicy parentPolicy)144 void setOwningPolicy(CarAudioService carAudioService, AudioPolicy parentPolicy) { 145 mAudioPolicy = parentPolicy; 146 mCarAudioService = carAudioService; 147 148 for (int i = 0; i < mFocusZones.size(); i++) { 149 mFocusZones.valueAt(i).setOwningPolicy(mAudioPolicy); 150 } 151 } 152 setRestrictFocus(boolean isFocusRestricted)153 void setRestrictFocus(boolean isFocusRestricted) { 154 int[] zoneIds = new int[mFocusZones.size()]; 155 for (int i = 0; i < mFocusZones.size(); i++) { 156 zoneIds[i] = mFocusZones.keyAt(i); 157 mFocusZones.valueAt(i).setRestrictFocus(isFocusRestricted); 158 } 159 notifyFocusListeners(zoneIds); 160 } 161 162 @Override onAudioFocusRequest(AudioFocusInfo afi, int requestResult)163 public void onAudioFocusRequest(AudioFocusInfo afi, int requestResult) { 164 int zoneId = getAudioZoneIdForAudioFocusInfo(afi); 165 getCarAudioFocusForZoneId(zoneId).onAudioFocusRequest(afi, requestResult); 166 notifyFocusListeners(new int[]{zoneId}); 167 } 168 169 /** 170 * @see AudioManager#abandonAudioFocus(AudioManager.OnAudioFocusChangeListener, AudioAttributes) 171 * Note that we'll get this call for a focus holder that dies while in the focus stack, so 172 * we don't need to watch for death notifications directly. 173 */ 174 @Override onAudioFocusAbandon(AudioFocusInfo afi)175 public void onAudioFocusAbandon(AudioFocusInfo afi) { 176 int zoneId = getAudioZoneIdForAudioFocusInfo(afi); 177 getCarAudioFocusForZoneId(zoneId).onAudioFocusAbandon(afi); 178 notifyFocusListeners(new int[]{zoneId}); 179 } 180 181 @NonNull getCarAudioFocusForZoneId(int zoneId)182 private CarAudioFocus getCarAudioFocusForZoneId(int zoneId) { 183 return mFocusZones.get(zoneId); 184 } 185 getAudioZoneIdForAudioFocusInfo(AudioFocusInfo afi)186 private int getAudioZoneIdForAudioFocusInfo(AudioFocusInfo afi) { 187 int zoneId = mCarAudioService.getZoneIdForUid(afi.getClientUid()); 188 189 // If the bundle attribute for AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID has been assigned 190 // Use zone id from that instead. 191 Bundle bundle = afi.getAttributes().getBundle(); 192 193 if (bundle != null) { 194 int bundleZoneId = 195 bundle.getInt(CarAudioManager.AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID, 196 -1); 197 // check if the zone id is within current zones bounds 198 if (mCarAudioService.isAudioZoneIdValid(bundleZoneId)) { 199 Slogf.d(TAG, "getFocusForAudioFocusInfo valid zoneId %d with bundle request for" 200 + " client %s", bundleZoneId, afi.getClientId()); 201 zoneId = bundleZoneId; 202 } else { 203 Slogf.w(TAG, "getFocusForAudioFocusInfo invalid zoneId %d with bundle request for " 204 + "client %s, dispatching focus request to zoneId %d", bundleZoneId, 205 afi.getClientId(), zoneId); 206 } 207 } 208 209 return zoneId; 210 } 211 notifyFocusListeners(int[] zoneIds)212 private void notifyFocusListeners(int[] zoneIds) { 213 SparseArray<List<AudioFocusInfo>> focusHoldersByZoneId = new SparseArray<>(zoneIds.length); 214 for (int i = 0; i < zoneIds.length; i++) { 215 int zoneId = zoneIds[i]; 216 focusHoldersByZoneId.put(zoneId, mFocusZones.get(zoneId).getAudioFocusHolders()); 217 sendFocusChangeToOemService(getCarAudioFocusForZoneId(zoneId), zoneId); 218 } 219 220 if (mCarFocusCallback == null) { 221 return; 222 } 223 mCarFocusCallback.onFocusChange(zoneIds, focusHoldersByZoneId); 224 } 225 sendFocusChangeToOemService(CarAudioFocus carAudioFocus, int zoneId)226 private void sendFocusChangeToOemService(CarAudioFocus carAudioFocus, int zoneId) { 227 CarOemProxyService proxy = CarLocalServices.getService(CarOemProxyService.class); 228 if (proxy == null || !proxy.isOemServiceEnabled() || !proxy.isOemServiceReady() 229 || proxy.getCarOemAudioFocusService() == null) { 230 return; 231 } 232 233 proxy.getCarOemAudioFocusService().notifyAudioFocusChange( 234 carAudioFocus.getAudioFocusHolders(), carAudioFocus.getAudioFocusLosers(), zoneId); 235 } 236 237 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)238 void dump(IndentingPrintWriter writer) { 239 writer.println("*CarZonesAudioFocus*"); 240 writer.increaseIndent(); 241 writer.printf("Has Focus Callback: %b\n", mCarFocusCallback != null); 242 writer.println("Car Zones Audio Focus Listeners:"); 243 writer.increaseIndent(); 244 for (int i = 0; i < mFocusZones.size(); i++) { 245 writer.printf("Zone Id: %d\n", mFocusZones.keyAt(i)); 246 writer.increaseIndent(); 247 mFocusZones.valueAt(i).dump(writer); 248 writer.decreaseIndent(); 249 } 250 writer.decreaseIndent(); 251 writer.decreaseIndent(); 252 } 253 updateUserForZoneId(int audioZoneId, @UserIdInt int userId)254 public void updateUserForZoneId(int audioZoneId, @UserIdInt int userId) { 255 Preconditions.checkArgument(mCarAudioService.isAudioZoneIdValid(audioZoneId), 256 "Invalid zoneId %d", audioZoneId); 257 mFocusZones.get(audioZoneId).getFocusInteraction().setUserIdForSettings(userId); 258 } 259 260 /** 261 * Callback to get notified of the active focus holders after any focus request or abandon call 262 */ 263 public interface CarFocusCallback { 264 /** 265 * Called after a focus request or abandon call is handled. 266 * 267 * @param audioZoneIds IDs of the zones where the changes took place 268 * @param focusHoldersByZoneId sparse array by zone ID, where each value is a list of 269 * {@link AudioFocusInfo}s holding focus in specified audio zone 270 */ onFocusChange(int[] audioZoneIds, @NonNull SparseArray<List<AudioFocusInfo>> focusHoldersByZoneId)271 void onFocusChange(int[] audioZoneIds, 272 @NonNull SparseArray<List<AudioFocusInfo>> focusHoldersByZoneId); 273 } 274 } 275