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.media.CarAudioManager; 24 import android.content.pm.PackageManager; 25 import android.media.AudioAttributes; 26 import android.media.AudioFocusInfo; 27 import android.media.AudioManager; 28 import android.media.audiopolicy.AudioPolicy; 29 import android.os.Bundle; 30 import android.util.IndentingPrintWriter; 31 import android.util.SparseArray; 32 33 import com.android.car.CarLog; 34 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.internal.util.Preconditions; 37 import com.android.server.utils.Slogf; 38 39 import java.util.ArrayList; 40 import java.util.List; 41 import java.util.Objects; 42 43 /** 44 * Implements {@link AudioPolicy.AudioPolicyFocusListener} 45 * 46 * <p><b>Note:</b> Manages audio focus on a per zone basis. 47 */ 48 final class CarZonesAudioFocus extends AudioPolicy.AudioPolicyFocusListener { 49 private static final String TAG = CarLog.tagFor(CarZonesAudioFocus.class); 50 51 private final CarFocusCallback mCarFocusCallback; 52 private final boolean mDelayedFocusEnabled; 53 private CarAudioService mCarAudioService; // Dynamically assigned just after construction 54 private AudioPolicy mAudioPolicy; // Dynamically assigned just after construction 55 56 private final SparseArray<CarAudioFocus> mFocusZones; 57 createCarZonesAudioFocus(@onNull AudioManager audioManager, @NonNull PackageManager packageManager, @NonNull SparseArray<CarAudioZone> carAudioZones, @NonNull CarAudioSettings carAudioSettings, boolean enableDelayedAudioFocus, CarFocusCallback carFocusCallback)58 public static CarZonesAudioFocus createCarZonesAudioFocus(@NonNull AudioManager audioManager, 59 @NonNull PackageManager packageManager, 60 @NonNull SparseArray<CarAudioZone> carAudioZones, 61 @NonNull CarAudioSettings carAudioSettings, 62 boolean enableDelayedAudioFocus, 63 CarFocusCallback carFocusCallback) { 64 Objects.requireNonNull(audioManager, "AudioManager cannot be null"); 65 Objects.requireNonNull(packageManager, "PackageManager cannot be null"); 66 Objects.requireNonNull(carAudioZones, "CarAudioZones cannot be null"); 67 Preconditions.checkArgument(carAudioZones.size() != 0, 68 "There must be a minimum of one audio zone"); 69 Objects.requireNonNull(carAudioSettings, "CarAudioSettings cannot be null"); 70 71 SparseArray<CarAudioFocus> audioFocusPerZone = new SparseArray<>(); 72 73 //Create focus for all the zones 74 for (int i = 0; i < carAudioZones.size(); i++) { 75 CarAudioZone audioZone = carAudioZones.valueAt(i); 76 int audioZoneId = audioZone.getId(); 77 Slogf.d(TAG, "Adding new zone %d", audioZoneId); 78 CarAudioFocus zoneFocusListener = 79 new CarAudioFocus(audioManager, packageManager, 80 new FocusInteraction(carAudioSettings), enableDelayedAudioFocus); 81 audioFocusPerZone.put(audioZoneId, zoneFocusListener); 82 } 83 return new CarZonesAudioFocus(audioFocusPerZone, enableDelayedAudioFocus, carFocusCallback); 84 } 85 86 @VisibleForTesting CarZonesAudioFocus(SparseArray<CarAudioFocus> focusZones, boolean enableDelayedAudioFocus, CarFocusCallback carFocusCallback)87 CarZonesAudioFocus(SparseArray<CarAudioFocus> focusZones, 88 boolean enableDelayedAudioFocus, 89 CarFocusCallback carFocusCallback) { 90 mFocusZones = focusZones; 91 mDelayedFocusEnabled = enableDelayedAudioFocus; 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 notifyFocusCallback(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 notifyFocusCallback(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 notifyFocusCallback(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 notifyFocusCallback(int[] zoneIds)212 private void notifyFocusCallback(int[] zoneIds) { 213 if (mCarFocusCallback == null) { 214 return; 215 } 216 SparseArray<List<AudioFocusInfo>> focusHoldersByZoneId = new SparseArray<>(); 217 for (int i = 0; i < zoneIds.length; i++) { 218 int zoneId = zoneIds[i]; 219 List<AudioFocusInfo> focusHolders = mFocusZones.get(zoneId).getAudioFocusHolders(); 220 focusHoldersByZoneId.put(zoneId, focusHolders); 221 } 222 223 mCarFocusCallback.onFocusChange(zoneIds, focusHoldersByZoneId); 224 } 225 226 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)227 void dump(IndentingPrintWriter writer) { 228 writer.println("*CarZonesAudioFocus*"); 229 writer.increaseIndent(); 230 writer.printf("Delayed Focus Enabled: %b\n", mDelayedFocusEnabled); 231 writer.printf("Has Focus Callback: %b\n", mCarFocusCallback != null); 232 writer.println("Car Zones Audio Focus Listeners:"); 233 writer.increaseIndent(); 234 for (int i = 0; i < mFocusZones.size(); i++) { 235 writer.printf("Zone Id: %d\n", mFocusZones.keyAt(i)); 236 writer.increaseIndent(); 237 mFocusZones.valueAt(i).dump(writer); 238 writer.decreaseIndent(); 239 } 240 writer.decreaseIndent(); 241 writer.decreaseIndent(); 242 } 243 updateUserForZoneId(int audioZoneId, @UserIdInt int userId)244 public void updateUserForZoneId(int audioZoneId, @UserIdInt int userId) { 245 Preconditions.checkArgument(mCarAudioService.isAudioZoneIdValid(audioZoneId), 246 "Invalid zoneId %d", audioZoneId); 247 mFocusZones.get(audioZoneId).getFocusInteraction().setUserIdForSettings(userId); 248 } 249 250 /** 251 * Callback to get notified of the active focus holders after any focus request or abandon call 252 */ 253 public interface CarFocusCallback { 254 /** 255 * Called after a focus request or abandon call is handled. 256 * 257 * @param audioZoneIds IDs of the zones where the changes took place 258 * @param focusHoldersByZoneId sparse array by zone ID, where each value is a list of 259 * {@link AudioFocusInfo}s holding focus in specified audio zone 260 */ onFocusChange(int[] audioZoneIds, @NonNull SparseArray<List<AudioFocusInfo>> focusHoldersByZoneId)261 void onFocusChange(int[] audioZoneIds, 262 @NonNull SparseArray<List<AudioFocusInfo>> focusHoldersByZoneId); 263 } 264 } 265