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