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.AudioDeviceInfo;
20 import android.util.Log;
21 import android.view.DisplayAddress;
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 final List<DisplayAddress.Physical> mPhysicalDisplayAddresses;
48 
CarAudioZone(int id, String name)49     CarAudioZone(int id, String name) {
50         mId = id;
51         mName = name;
52         mVolumeGroups = new ArrayList<>();
53         mPhysicalDisplayAddresses = 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 (int busNumber : group.getBusNumbers()) {
85                 devices.add(group.getCarAudioDeviceInfoForBus(busNumber).getAudioDeviceInfo());
86             }
87         }
88         return devices;
89     }
90 
getVolumeGroupCount()91     int getVolumeGroupCount() {
92         return mVolumeGroups.size();
93     }
94 
95     /**
96      * Associates a new display physical port with this audio zone. This can be used to
97      * identify what zone an activity should produce sound in when launching on a particular display
98      * @param physicalDisplayAddress port to associate with this zone
99      */
addPhysicalDisplayAddress(DisplayAddress.Physical physicalDisplayAddress)100     void addPhysicalDisplayAddress(DisplayAddress.Physical physicalDisplayAddress) {
101         mPhysicalDisplayAddresses.add(physicalDisplayAddress);
102     }
103 
104     /**
105      * Gets list of ports for displays associated with this audio zone
106      * @return list of Physical ports for displays associated with this audio zone
107      */
getPhysicalDisplayAddresses()108     List<DisplayAddress.Physical> getPhysicalDisplayAddresses() {
109         return mPhysicalDisplayAddresses;
110     }
111 
112     /**
113      * @return Snapshot of available {@link CarVolumeGroup}s in array.
114      */
getVolumeGroups()115     CarVolumeGroup[] getVolumeGroups() {
116         return mVolumeGroups.toArray(new CarVolumeGroup[0]);
117     }
118 
119     /**
120      * Constraints applied here:
121      *
122      * - One context should not appear in two groups
123      * - All contexts are assigned
124      * - One bus should not appear in two groups
125      * - All gain controllers in the same group have same step value
126      *
127      * Note that it is fine that there are buses not appear in any group, those buses may be
128      * reserved for other usages.
129      * Step value validation is done in {@link CarVolumeGroup#bind(int, int, CarAudioDeviceInfo)}
130      */
validateVolumeGroups()131     boolean validateVolumeGroups() {
132         Set<Integer> contextSet = new HashSet<>();
133         Set<Integer> busNumberSet = new HashSet<>();
134         for (CarVolumeGroup group : mVolumeGroups) {
135             // One context should not appear in two groups
136             for (int context : group.getContexts()) {
137                 if (contextSet.contains(context)) {
138                     Log.e(CarLog.TAG_AUDIO, "Context appears in two groups: " + context);
139                     return false;
140                 }
141                 contextSet.add(context);
142             }
143 
144             // One bus should not appear in two groups
145             for (int busNumber : group.getBusNumbers()) {
146                 if (busNumberSet.contains(busNumber)) {
147                     Log.e(CarLog.TAG_AUDIO, "Bus appears in two groups: " + busNumber);
148                     return false;
149                 }
150                 busNumberSet.add(busNumber);
151             }
152         }
153 
154         // All contexts are assigned
155         if (contextSet.size() != CarAudioDynamicRouting.CONTEXT_NUMBERS.length) {
156             Log.e(CarLog.TAG_AUDIO, "Some contexts are not assigned to group");
157             Log.e(CarLog.TAG_AUDIO, "Assigned contexts "
158                     + Arrays.toString(contextSet.toArray(new Integer[0])));
159             Log.e(CarLog.TAG_AUDIO,
160                     "All contexts " + Arrays.toString(CarAudioDynamicRouting.CONTEXT_NUMBERS));
161             return false;
162         }
163 
164         return true;
165     }
166 
synchronizeCurrentGainIndex()167     void synchronizeCurrentGainIndex() {
168         for (CarVolumeGroup group : mVolumeGroups) {
169             group.setCurrentGainIndex(group.getCurrentGainIndex());
170         }
171     }
172 
dump(String indent, PrintWriter writer)173     void dump(String indent, PrintWriter writer) {
174         writer.printf("%sCarAudioZone(%s:%d) isPrimary? %b\n", indent, mName, mId, isPrimaryZone());
175         for (DisplayAddress.Physical physical: mPhysicalDisplayAddresses) {
176             long port = (long) physical.getPort();
177             writer.printf("%sDisplayAddress.Physical(%d)\n", indent + "\t", port);
178         }
179         writer.println();
180 
181         for (CarVolumeGroup group : mVolumeGroups) {
182             group.dump(indent + "\t", writer);
183         }
184         writer.println();
185     }
186 }
187