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 package com.android.car.audio; 17 18 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 19 20 import android.car.builtin.util.Slogf; 21 import android.car.media.CarAudioManager; 22 import android.car.media.CarVolumeGroupInfo; 23 import android.media.AudioAttributes; 24 import android.media.AudioDeviceAttributes; 25 import android.media.AudioDeviceInfo; 26 import android.media.AudioPlaybackConfiguration; 27 import android.util.ArraySet; 28 29 import com.android.car.CarLog; 30 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 31 import com.android.car.internal.util.IndentingPrintWriter; 32 import com.android.internal.util.Preconditions; 33 34 import java.util.ArrayList; 35 import java.util.HashSet; 36 import java.util.List; 37 import java.util.Objects; 38 import java.util.Set; 39 40 /** 41 * A class encapsulates an audio zone in car. 42 * 43 * An audio zone can contain multiple {@link CarVolumeGroup}s, and each zone has its own 44 * {@link CarAudioFocus} instance. Additionally, there may be dedicated hardware volume keys 45 * attached to each zone. 46 * 47 * See also the unified car_audio_configuration.xml 48 */ 49 public class CarAudioZone { 50 51 private final int mId; 52 private final String mName; 53 private final List<CarVolumeGroup> mVolumeGroups; 54 private final Set<String> mDeviceAddresses; 55 private final CarAudioContext mCarAudioContext; 56 private List<AudioDeviceAttributes> mInputAudioDevice; 57 CarAudioZone(CarAudioContext carAudioContext, String name, int id)58 CarAudioZone(CarAudioContext carAudioContext, String name, int id) { 59 mCarAudioContext = Objects.requireNonNull(carAudioContext, 60 "Car audio context can not be null"); 61 mName = name; 62 mId = id; 63 mVolumeGroups = new ArrayList<>(); 64 mInputAudioDevice = new ArrayList<>(); 65 mDeviceAddresses = new HashSet<>(); 66 } 67 getId()68 int getId() { 69 return mId; 70 } 71 getName()72 String getName() { 73 return mName; 74 } 75 isPrimaryZone()76 boolean isPrimaryZone() { 77 return mId == CarAudioManager.PRIMARY_AUDIO_ZONE; 78 } 79 addVolumeGroup(CarVolumeGroup volumeGroup)80 void addVolumeGroup(CarVolumeGroup volumeGroup) { 81 mVolumeGroups.add(volumeGroup); 82 mDeviceAddresses.addAll(volumeGroup.getAddresses()); 83 } 84 getVolumeGroup(int groupId)85 CarVolumeGroup getVolumeGroup(int groupId) { 86 Preconditions.checkArgumentInRange(groupId, 0, mVolumeGroups.size() - 1, 87 "groupId(" + groupId + ") is out of range"); 88 return mVolumeGroups.get(groupId); 89 } 90 91 /** 92 * @return Snapshot of available {@link AudioDeviceInfo}s in List. 93 */ getAudioDeviceInfos()94 List<AudioDeviceInfo> getAudioDeviceInfos() { 95 final List<AudioDeviceInfo> devices = new ArrayList<>(); 96 for (CarVolumeGroup group : mVolumeGroups) { 97 for (String address : group.getAddresses()) { 98 devices.add(group.getCarAudioDeviceInfoForAddress(address).getAudioDeviceInfo()); 99 } 100 } 101 return devices; 102 } 103 getVolumeGroupCount()104 int getVolumeGroupCount() { 105 return mVolumeGroups.size(); 106 } 107 108 /** 109 * @return Snapshot of available {@link CarVolumeGroup}s in array. 110 */ getVolumeGroups()111 CarVolumeGroup[] getVolumeGroups() { 112 return mVolumeGroups.toArray(new CarVolumeGroup[0]); 113 } 114 115 /** 116 * Constraints applied here: 117 * 118 * - One context should not appear in two groups 119 * - All contexts are assigned 120 * - One device should not appear in two groups 121 * - All gain controllers in the same group have same step value 122 * 123 * Note that it is fine that there are devices which do not appear in any group. Those devices 124 * may be reserved for other purposes. 125 * Step value validation is done in 126 * {@link CarVolumeGroup.Builder#setDeviceInfoForContext(int, CarAudioDeviceInfo)} 127 */ validateVolumeGroups()128 boolean validateVolumeGroups() { 129 ArraySet<Integer> contexts = new ArraySet<>(); 130 ArraySet<String> addresses = new ArraySet<>(); 131 for (int index = 0; index < mVolumeGroups.size(); index++) { 132 CarVolumeGroup group = mVolumeGroups.get(index); 133 // One context should not appear in two groups 134 int[] groupContexts = group.getContexts(); 135 for (int groupIndex = 0; groupIndex < groupContexts.length; groupIndex++) { 136 int contextId = groupContexts[groupIndex]; 137 if (!contexts.add(contextId)) { 138 Slogf.e(CarLog.TAG_AUDIO, "Context appears in two groups %d", contextId); 139 return false; 140 } 141 } 142 143 // One address should not appear in two groups 144 List<String> groupAddresses = group.getAddresses(); 145 for (int addressIndex = 0; addressIndex < groupAddresses.size(); addressIndex++) { 146 String address = groupAddresses.get(addressIndex); 147 if (!addresses.add(address)) { 148 Slogf.e(CarLog.TAG_AUDIO, "Address appears in two groups: " + address); 149 return false; 150 } 151 } 152 } 153 154 boolean allContextValidated = true; 155 List<Integer> allContexts = mCarAudioContext.getAllContextsIds(); 156 for (int index = 0; index < allContexts.size(); index++) { 157 if (!contexts.contains(allContexts.get(index))) { 158 Slogf.e(CarLog.TAG_AUDIO, "Audio context %s is not assigned to a group", 159 mCarAudioContext.toString(allContexts.get(index))); 160 allContextValidated = false; 161 } 162 } 163 164 if (!allContextValidated) { 165 return false; 166 } 167 168 List<Integer> contextList = new ArrayList<>(contexts); 169 // All contexts are assigned 170 if (!mCarAudioContext.validateAllAudioAttributesSupported(contextList)) { 171 Slogf.e(CarLog.TAG_AUDIO, "Some audio attributes are not assigned to a group"); 172 return false; 173 } 174 return true; 175 } 176 synchronizeCurrentGainIndex()177 void synchronizeCurrentGainIndex() { 178 for (CarVolumeGroup group : mVolumeGroups) { 179 group.setCurrentGainIndex(group.getCurrentGainIndex()); 180 } 181 } 182 183 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)184 void dump(IndentingPrintWriter writer) { 185 writer.printf("CarAudioZone(%s:%d) isPrimary? %b\n", mName, mId, isPrimaryZone()); 186 writer.increaseIndent(); 187 for (CarVolumeGroup group : mVolumeGroups) { 188 group.dump(writer); 189 } 190 191 writer.printf("Input Audio Device Addresses\n"); 192 writer.increaseIndent(); 193 for (AudioDeviceAttributes audioDevice : mInputAudioDevice) { 194 writer.printf("Device Address(%s)\n", audioDevice.getAddress()); 195 } 196 writer.decreaseIndent(); 197 writer.println(); 198 writer.decreaseIndent(); 199 } 200 201 /** 202 * Return the audio device address mapping to a car audio context 203 */ getAddressForContext(int audioContext)204 public String getAddressForContext(int audioContext) { 205 mCarAudioContext.preconditionCheckAudioContext(audioContext); 206 String deviceAddress = null; 207 for (CarVolumeGroup volumeGroup : getVolumeGroups()) { 208 deviceAddress = volumeGroup.getAddressForContext(audioContext); 209 if (deviceAddress != null) { 210 return deviceAddress; 211 } 212 } 213 // This should not happen unless something went wrong. 214 // Device address are unique per zone and all contexts are assigned in a zone. 215 throw new IllegalStateException("Could not find output device in zone " + mId 216 + " for audio context " + audioContext); 217 } 218 getAudioDeviceForContext(int audioContext)219 public AudioDeviceInfo getAudioDeviceForContext(int audioContext) { 220 mCarAudioContext.preconditionCheckAudioContext(audioContext); 221 for (CarVolumeGroup volumeGroup : getVolumeGroups()) { 222 AudioDeviceInfo deviceInfo = volumeGroup.getAudioDeviceForContext(audioContext); 223 if (deviceInfo != null) { 224 return deviceInfo; 225 } 226 } 227 // This should not happen unless something went wrong. 228 // Device address are unique per zone and all contexts are assigned in a zone. 229 throw new IllegalStateException("Could not find output device in zone " + mId 230 + " for audio context " + audioContext); 231 } 232 233 /** 234 * Update the volume groups for the new user 235 * @param userId user id to update to 236 */ updateVolumeGroupsSettingsForUser(int userId)237 public void updateVolumeGroupsSettingsForUser(int userId) { 238 for (CarVolumeGroup group : mVolumeGroups) { 239 group.loadVolumesSettingsForUser(userId); 240 } 241 } 242 addInputAudioDevice(AudioDeviceAttributes device)243 void addInputAudioDevice(AudioDeviceAttributes device) { 244 mInputAudioDevice.add(device); 245 } 246 getInputAudioDevices()247 List<AudioDeviceAttributes> getInputAudioDevices() { 248 return mInputAudioDevice; 249 } 250 findActiveAudioAttributesFromPlaybackConfigurations( List<AudioPlaybackConfiguration> configurations)251 public List<AudioAttributes> findActiveAudioAttributesFromPlaybackConfigurations( 252 List<AudioPlaybackConfiguration> configurations) { 253 Objects.requireNonNull(configurations, "Audio playback configurations can not be null"); 254 List<AudioAttributes> audioAttributes = new ArrayList<>(); 255 for (int index = 0; index < configurations.size(); index++) { 256 AudioPlaybackConfiguration configuration = configurations.get(index); 257 if (configuration.isActive()) { 258 if (isAudioDeviceInfoValidForZone(configuration.getAudioDeviceInfo())) { 259 // Note that address's context and the context actually supplied could be 260 // different 261 audioAttributes.add(configuration.getAudioAttributes()); 262 } 263 } 264 } 265 return audioAttributes; 266 } 267 isAudioDeviceInfoValidForZone(AudioDeviceInfo info)268 boolean isAudioDeviceInfoValidForZone(AudioDeviceInfo info) { 269 return info != null 270 && info.getAddress() != null 271 && !info.getAddress().isEmpty() 272 && containsDeviceAddress(info.getAddress()); 273 } 274 containsDeviceAddress(String deviceAddress)275 private boolean containsDeviceAddress(String deviceAddress) { 276 return mDeviceAddresses.contains(deviceAddress); 277 } 278 onAudioGainChanged(List<Integer> halReasons, List<CarAudioGainConfigInfo> gains)279 void onAudioGainChanged(List<Integer> halReasons, List<CarAudioGainConfigInfo> gains) { 280 for (int index = 0; index < gains.size(); index++) { 281 CarAudioGainConfigInfo gainInfo = gains.get(index); 282 for (int groupIndex = 0; groupIndex < mVolumeGroups.size(); groupIndex++) { 283 CarVolumeGroup group = mVolumeGroups.get(groupIndex); 284 if (group.getAddresses().contains(gainInfo.getDeviceAddress())) { 285 group.onAudioGainChanged(halReasons, gainInfo); 286 break; // loop of CarVolumeGroup. 287 } 288 } 289 } 290 } 291 292 /** 293 * Returns the car audio context set for the car audio zone 294 */ getCarAudioContext()295 public CarAudioContext getCarAudioContext() { 296 return mCarAudioContext; 297 } 298 299 /** 300 * Returns the car volume infos for all the volume groups in the audio zone 301 */ getVolumeGroupInfos()302 List<CarVolumeGroupInfo> getVolumeGroupInfos() { 303 List<CarVolumeGroupInfo> groupInfos = new ArrayList<>(mVolumeGroups.size()); 304 for (int index = 0; index < mVolumeGroups.size(); index++) { 305 groupInfos.add(mVolumeGroups.get(index).getCarVolumeGroupInfo()); 306 } 307 308 return groupInfos; 309 } 310 } 311