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.annotation.NonNull; 21 import android.car.media.CarAudioManager; 22 import android.media.AudioDeviceAttributes; 23 import android.media.AudioDeviceInfo; 24 import android.media.AudioPlaybackConfiguration; 25 import android.util.IndentingPrintWriter; 26 import android.util.Slog; 27 28 import com.android.car.CarLog; 29 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 30 import com.android.internal.util.Preconditions; 31 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.HashSet; 35 import java.util.List; 36 import java.util.Objects; 37 import java.util.Set; 38 39 /** 40 * A class encapsulates an audio zone in car. 41 * 42 * An audio zone can contain multiple {@link CarVolumeGroup}s, and each zone has its own 43 * {@link CarAudioFocus} instance. Additionally, there may be dedicated hardware volume keys 44 * attached to each zone. 45 * 46 * See also the unified car_audio_configuration.xml 47 */ 48 /* package */ class CarAudioZone { 49 50 private final int mId; 51 private final String mName; 52 private final List<CarVolumeGroup> mVolumeGroups; 53 private final Set<String> mDeviceAddresses; 54 private List<AudioDeviceAttributes> mInputAudioDevice; 55 CarAudioZone(int id, String name)56 CarAudioZone(int id, String name) { 57 mId = id; 58 mName = name; 59 mVolumeGroups = new ArrayList<>(); 60 mInputAudioDevice = new ArrayList<>(); 61 mDeviceAddresses = new HashSet<>(); 62 } 63 getId()64 int getId() { 65 return mId; 66 } 67 getName()68 String getName() { 69 return mName; 70 } 71 isPrimaryZone()72 boolean isPrimaryZone() { 73 return mId == CarAudioManager.PRIMARY_AUDIO_ZONE; 74 } 75 addVolumeGroup(CarVolumeGroup volumeGroup)76 void addVolumeGroup(CarVolumeGroup volumeGroup) { 77 mVolumeGroups.add(volumeGroup); 78 mDeviceAddresses.addAll(volumeGroup.getAddresses()); 79 } 80 getVolumeGroup(int groupId)81 CarVolumeGroup getVolumeGroup(int groupId) { 82 Preconditions.checkArgumentInRange(groupId, 0, mVolumeGroups.size() - 1, 83 "groupId(" + groupId + ") is out of range"); 84 return mVolumeGroups.get(groupId); 85 } 86 87 /** 88 * @return Snapshot of available {@link AudioDeviceInfo}s in List. 89 */ getAudioDeviceInfos()90 List<AudioDeviceInfo> getAudioDeviceInfos() { 91 final List<AudioDeviceInfo> devices = new ArrayList<>(); 92 for (CarVolumeGroup group : mVolumeGroups) { 93 for (String address : group.getAddresses()) { 94 devices.add(group.getCarAudioDeviceInfoForAddress(address).getAudioDeviceInfo()); 95 } 96 } 97 return devices; 98 } 99 getVolumeGroupCount()100 int getVolumeGroupCount() { 101 return mVolumeGroups.size(); 102 } 103 104 /** 105 * @return Snapshot of available {@link CarVolumeGroup}s in array. 106 */ getVolumeGroups()107 CarVolumeGroup[] getVolumeGroups() { 108 return mVolumeGroups.toArray(new CarVolumeGroup[0]); 109 } 110 111 /** 112 * Constraints applied here: 113 * 114 * - One context should not appear in two groups 115 * - All contexts are assigned 116 * - One device should not appear in two groups 117 * - All gain controllers in the same group have same step value 118 * 119 * Note that it is fine that there are devices which do not appear in any group. Those devices 120 * may be reserved for other purposes. 121 * Step value validation is done in 122 * {@link CarVolumeGroup.Builder#setDeviceInfoForContext(int, CarAudioDeviceInfo)} 123 */ validateVolumeGroups()124 boolean validateVolumeGroups() { 125 Set<Integer> contexts = new HashSet<>(); 126 Set<String> addresses = new HashSet<>(); 127 for (int index = 0; index < mVolumeGroups.size(); index++) { 128 CarVolumeGroup group = mVolumeGroups.get(index); 129 // One context should not appear in two groups 130 for (int context : group.getContexts()) { 131 if (!contexts.add(context)) { 132 Slog.e(CarLog.TAG_AUDIO, "Context appears in two groups: " + context); 133 return false; 134 } 135 } 136 137 // One address should not appear in two groups 138 for (String address : group.getAddresses()) { 139 if (!addresses.add(address)) { 140 Slog.e(CarLog.TAG_AUDIO, "Address appears in two groups: " + address); 141 return false; 142 } 143 } 144 } 145 146 // All contexts are assigned 147 if (contexts.size() != CarAudioContext.CONTEXTS.length) { 148 Slog.e(CarLog.TAG_AUDIO, "Some contexts are not assigned to group"); 149 Slog.e(CarLog.TAG_AUDIO, "Assigned contexts " + contexts); 150 Slog.e(CarLog.TAG_AUDIO, 151 "All contexts " + Arrays.toString(CarAudioContext.CONTEXTS)); 152 return false; 153 } 154 return true; 155 } 156 synchronizeCurrentGainIndex()157 void synchronizeCurrentGainIndex() { 158 for (CarVolumeGroup group : mVolumeGroups) { 159 group.setCurrentGainIndex(group.getCurrentGainIndex()); 160 } 161 } 162 163 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)164 void dump(IndentingPrintWriter writer) { 165 writer.printf("CarAudioZone(%s:%d) isPrimary? %b\n", mName, mId, isPrimaryZone()); 166 writer.increaseIndent(); 167 for (CarVolumeGroup group : mVolumeGroups) { 168 group.dump(writer); 169 } 170 171 writer.printf("Input Audio Device Addresses\n"); 172 writer.increaseIndent(); 173 for (AudioDeviceAttributes audioDevice : mInputAudioDevice) { 174 writer.printf("Device Address(%s)\n", audioDevice.getAddress()); 175 } 176 writer.decreaseIndent(); 177 writer.println(); 178 writer.decreaseIndent(); 179 } 180 getAddressForContext(int audioContext)181 String getAddressForContext(int audioContext) { 182 CarAudioContext.preconditionCheckAudioContext(audioContext); 183 String deviceAddress = null; 184 for (CarVolumeGroup volumeGroup : getVolumeGroups()) { 185 deviceAddress = volumeGroup.getAddressForContext(audioContext); 186 if (deviceAddress != null) { 187 return deviceAddress; 188 } 189 } 190 // This should not happen unless something went wrong. 191 // Device address are unique per zone and all contexts are assigned in a zone. 192 throw new IllegalStateException("Could not find output device in zone " + mId 193 + " for audio context " + audioContext); 194 } 195 196 /** 197 * Update the volume groups for the new user 198 * @param userId user id to update to 199 */ updateVolumeGroupsSettingsForUser(int userId)200 public void updateVolumeGroupsSettingsForUser(int userId) { 201 for (CarVolumeGroup group : mVolumeGroups) { 202 group.loadVolumesSettingsForUser(userId); 203 } 204 } 205 addInputAudioDevice(AudioDeviceAttributes device)206 void addInputAudioDevice(AudioDeviceAttributes device) { 207 mInputAudioDevice.add(device); 208 } 209 getInputAudioDevices()210 List<AudioDeviceAttributes> getInputAudioDevices() { 211 return mInputAudioDevice; 212 } 213 findActiveContextsFromPlaybackConfigurations( @onNull List<AudioPlaybackConfiguration> configurations)214 public @NonNull List<Integer> findActiveContextsFromPlaybackConfigurations( 215 @NonNull List<AudioPlaybackConfiguration> configurations) { 216 Objects.requireNonNull(configurations); 217 List<Integer> activeContexts = new ArrayList<>(); 218 for (int index = 0; index < configurations.size(); index++) { 219 AudioPlaybackConfiguration configuration = configurations.get(index); 220 if (configuration.isActive()) { 221 if (isAudioDeviceInfoValidForZone(configuration.getAudioDeviceInfo())) { 222 // Note that address's context and the context actually supplied could be 223 // different 224 activeContexts.add(CarAudioContext.getContextForUsage( 225 configuration.getAudioAttributes().getSystemUsage())); 226 } 227 } 228 } 229 return activeContexts; 230 } 231 isAudioDeviceInfoValidForZone(AudioDeviceInfo info)232 boolean isAudioDeviceInfoValidForZone(AudioDeviceInfo info) { 233 return info != null 234 && info.getAddress() != null 235 && !info.getAddress().isEmpty() 236 && containsDeviceAddress(info.getAddress()); 237 } 238 containsDeviceAddress(String deviceAddress)239 private boolean containsDeviceAddress(String deviceAddress) { 240 return mDeviceAddresses.contains(deviceAddress); 241 } 242 } 243