1 /*
2  * Copyright (C) 2023 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 android.car.media;
18 
19 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE;
20 
21 import static java.util.Collections.EMPTY_LIST;
22 
23 import android.annotation.FlaggedApi;
24 import android.annotation.NonNull;
25 import android.annotation.SystemApi;
26 import android.car.feature.Flags;
27 import android.os.Parcel;
28 import android.os.Parcelable;
29 import android.util.ArraySet;
30 
31 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
32 import com.android.internal.annotations.VisibleForTesting;
33 
34 import java.util.ArrayList;
35 import java.util.List;
36 import java.util.Objects;
37 import java.util.Set;
38 import java.util.concurrent.Executor;
39 
40 /**
41  * Class to encapsulate car audio zone configuration information.
42  *
43  * @hide
44  */
45 @SystemApi
46 public final class CarAudioZoneConfigInfo implements Parcelable {
47 
48     private final String mName;
49     private final int mZoneId;
50     private final int mConfigId;
51     private final boolean mIsConfigActive;
52     private final boolean mIsConfigSelected;
53     private final boolean mIsDefault;
54     private final List<CarVolumeGroupInfo> mConfigVolumeGroups;
55 
56     /**
57      * Constructor of car audio zone configuration info
58      *
59      * @param name Name for car audio zone configuration info
60      * @param zoneId Id of car audio zone
61      * @param configId Id of car audio zone configuration info
62      *
63      * @hide
64      */
CarAudioZoneConfigInfo(String name, int zoneId, int configId)65     public CarAudioZoneConfigInfo(String name, int zoneId, int configId) {
66         this(name, EMPTY_LIST, zoneId, configId, /* isActive= */ true, /* isSelected= */ false,
67                 /* isDefault= */ false);
68     }
69 
70     /**
71      * Constructor of car audio zone configuration info
72      *
73      * @param name       Name for car audio zone configuration info
74      * @param groups     Volume groups for the audio zone configuration
75      * @param zoneId     Id of car audio zone
76      * @param configId   Id of car audio zone configuration info
77      * @param isActive   Active status of the audio configuration
78      * @param isSelected Selected status of the audio configuration
79      * @param isDefault  Determines if the audio configuration is default
80      *
81      * @hide
82      */
CarAudioZoneConfigInfo(String name, List<CarVolumeGroupInfo> groups, int zoneId, int configId, boolean isActive, boolean isSelected, boolean isDefault)83     public CarAudioZoneConfigInfo(String name, List<CarVolumeGroupInfo> groups, int zoneId,
84             int configId, boolean isActive, boolean isSelected, boolean isDefault) {
85         mName = Objects.requireNonNull(name, "Zone configuration name can not be null");
86         mConfigVolumeGroups = Objects.requireNonNull(groups,
87                 "Zone configuration volume groups can not be null");
88         mZoneId = zoneId;
89         mConfigId = configId;
90         mIsConfigActive = isActive;
91         mIsConfigSelected = isSelected;
92         mIsDefault = isDefault;
93     }
94 
95     /**
96      * Creates zone configuration info from parcel
97      *
98      * @hide
99      */
100     @VisibleForTesting
CarAudioZoneConfigInfo(Parcel in)101     public CarAudioZoneConfigInfo(Parcel in) {
102         mName = in.readString();
103         mZoneId = in.readInt();
104         mConfigId = in.readInt();
105         mIsConfigActive = in.readBoolean();
106         mIsConfigSelected = in.readBoolean();
107         mIsDefault = in.readBoolean();
108         List<CarVolumeGroupInfo> volumeGroups = new ArrayList<>();
109         in.readParcelableList(volumeGroups, CarVolumeGroupInfo.class.getClassLoader(),
110                 CarVolumeGroupInfo.class);
111         mConfigVolumeGroups = volumeGroups;
112     }
113 
114     @NonNull
115     public static final Creator<CarAudioZoneConfigInfo> CREATOR = new Creator<>() {
116         @Override
117         @NonNull
118         public CarAudioZoneConfigInfo createFromParcel(@NonNull Parcel in) {
119             return new CarAudioZoneConfigInfo(in);
120         }
121 
122         @Override
123         @NonNull
124         public CarAudioZoneConfigInfo[] newArray(int size) {
125             return new CarAudioZoneConfigInfo[size];
126         }
127     };
128 
129     @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
130     @Override
describeContents()131     public int describeContents() {
132         return 0;
133     }
134 
135     /**
136      * Returns the car audio zone configuration name
137      */
138     @NonNull
getName()139     public String getName() {
140         return mName;
141     }
142 
143     /**
144      * Returns the car audio zone id
145      */
getZoneId()146     public int getZoneId() {
147         return mZoneId;
148     }
149 
150     /**
151      * Returns the car audio zone configuration id
152      */
getConfigId()153     public int getConfigId() {
154         return mConfigId;
155     }
156 
157     @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
158     @Override
toString()159     public String toString() {
160         StringBuilder builder = new StringBuilder()
161                 .append("CarAudioZoneConfigInfo { name = ").append(mName)
162                 .append(", zone id = ").append(mZoneId)
163                 .append(", config id = ").append(mConfigId);
164 
165         if (Flags.carAudioDynamicDevices()) {
166             builder.append(", is active = ").append(mIsConfigActive)
167                     .append(", is selected = ").append(mIsConfigSelected)
168                     .append(", is default = ").append(mIsDefault)
169                     .append(", volume groups = ").append(mConfigVolumeGroups);
170         }
171 
172         builder.append(" }");
173         return builder.toString();
174     }
175 
176     @Override
writeToParcel(@onNull Parcel dest, int flags)177     public void writeToParcel(@NonNull Parcel dest, int flags) {
178         dest.writeString(mName);
179         dest.writeInt(mZoneId);
180         dest.writeInt(mConfigId);
181         dest.writeBoolean(mIsConfigActive);
182         dest.writeBoolean(mIsConfigSelected);
183         dest.writeBoolean(mIsDefault);
184         dest.writeParcelableList(mConfigVolumeGroups, flags);
185     }
186 
187     @Override
equals(Object o)188     public boolean equals(Object o) {
189         if (this == o) {
190             return true;
191         }
192 
193         if (!(o instanceof CarAudioZoneConfigInfo)) {
194             return false;
195         }
196 
197         CarAudioZoneConfigInfo that = (CarAudioZoneConfigInfo) o;
198         if (Flags.carAudioDynamicDevices()) {
199             return hasSameConfigInfoInternal(that) && mIsConfigActive == that.mIsConfigActive
200                     && mIsConfigSelected == that.mIsConfigSelected && mIsDefault == that.mIsDefault
201                     && hasSameVolumeGroup(that.mConfigVolumeGroups);
202         }
203 
204         return hasSameConfigInfoInternal(that);
205     }
206 
hasSameVolumeGroup(List<CarVolumeGroupInfo> carVolumeGroupInfos)207     private boolean hasSameVolumeGroup(List<CarVolumeGroupInfo> carVolumeGroupInfos) {
208         if (mConfigVolumeGroups.size() != carVolumeGroupInfos.size()) {
209             return false;
210         }
211         Set<CarVolumeGroupInfo> groups = new ArraySet<>(carVolumeGroupInfos);
212         for (int c = 0; c < mConfigVolumeGroups.size(); c++) {
213             if (groups.contains(mConfigVolumeGroups.get(c))) {
214                 groups.remove(mConfigVolumeGroups.get(c));
215                 continue;
216             }
217             return false;
218         }
219         return groups.isEmpty();
220     }
221 
222     /**
223      * Determines if the configuration has the same name, zone id, and config id
224      *
225      * @return {@code true} if the name, zone id, config id all match, {@code false} otherwise.
226      */
227     @FlaggedApi(Flags.FLAG_CAR_AUDIO_DYNAMIC_DEVICES)
hasSameConfigInfo(@onNull CarAudioZoneConfigInfo info)228     public boolean hasSameConfigInfo(@NonNull CarAudioZoneConfigInfo info) {
229         return hasSameConfigInfoInternal(Objects.requireNonNull(info,
230                 "Car audio zone info can not be null"));
231     }
232 
233     @Override
hashCode()234     public int hashCode() {
235         if (Flags.carAudioDynamicDevices()) {
236             return Objects.hash(mName, mZoneId, mConfigId, mIsConfigActive, mIsConfigSelected,
237                     mIsDefault, mConfigVolumeGroups);
238         }
239         return Objects.hash(mName, mZoneId, mConfigId);
240     }
241 
242     /**
243      * Determines if the configuration is active.
244      *
245      * <p>A configuration will be consider active if all the audio devices in the configuration
246      * are currently active, including those device which are dynamic
247      * (e.g. Bluetooth or wired headset).
248      *
249      * @return {@code true} if the configuration is active and can be selected for audio routing,
250      * {@code false} otherwise.
251      */
252     @FlaggedApi(Flags.FLAG_CAR_AUDIO_DYNAMIC_DEVICES)
isActive()253     public boolean isActive() {
254         return mIsConfigActive;
255     }
256 
257     /**
258      * Determines if the configuration is selected.
259      *
260      * @return if the configuration is currently selected for routing either by default or through
261      * audio configuration selection via the {@link CarAudioManager#switchAudioZoneToConfig} API,
262      * {@code true} if the configuration is currently selected, {@code false} otherwise.
263      */
264     @FlaggedApi(Flags.FLAG_CAR_AUDIO_DYNAMIC_DEVICES)
isSelected()265     public boolean isSelected() {
266         return mIsConfigSelected;
267     }
268 
269     /**
270      * Determines if the configuration is the default configuration.
271      *
272      * <p>The default configuration is used by the audio system to automatically route audio upon
273      * other configurations becoming inactive. The default configuration is also used for routing
274      * upon car audio service initialization. To switch to a non default configuration the
275      * {@link CarAudioManager#switchAudioZoneToConfig(CarAudioZoneConfigInfo, Executor,
276      * SwitchAudioZoneConfigCallback)} API must be called with the desired configuration.
277      *
278      * <p><b>Note</b> Each zone only has one default configuration.
279      *
280      * @return {@code true} if the configuration is the default configuration,
281      * {@code false} otherwise.
282      */
283     @FlaggedApi(Flags.FLAG_CAR_AUDIO_DYNAMIC_DEVICES)
isDefault()284     public boolean isDefault() {
285         return mIsDefault;
286     }
287 
288     /**
289      * Gets all audio volume group infos that belong to the audio configuration
290      *
291      * <p>This can be query to determine what audio device attributes are available to the volume
292      * group.
293      *
294      * <p><b>Note</b> This information should not be used for managing volume groups at run time,
295      * as the currently selected configuration may be different. Instead,
296      * {@link CarAudioManager#getAudioZoneConfigInfos} should be queried for the currently
297      * selected configuration for the audio zone.
298      *
299      * @return list of volume groups which belong to the audio configuration.
300      */
301     @FlaggedApi(Flags.FLAG_CAR_AUDIO_DYNAMIC_DEVICES)
302     @NonNull
getConfigVolumeGroups()303     public List<CarVolumeGroupInfo> getConfigVolumeGroups() {
304         return List.copyOf(mConfigVolumeGroups);
305     }
306 
hasSameConfigInfoInternal(CarAudioZoneConfigInfo info)307     private boolean hasSameConfigInfoInternal(CarAudioZoneConfigInfo info) {
308         return info == null ? false : (mName.equals(info.mName) && mZoneId == info.mZoneId
309                 && mConfigId == info.mConfigId);
310     }
311 
312     /**
313      * A builder for {@link CarAudioZoneConfigInfo}
314      *
315      * @hide
316      */
317     @SuppressWarnings("WeakerAccess")
318     public static final class Builder {
319 
320         private static final long IS_USED_FIELD_SET = 0x01;
321 
322         private final String mName;
323         private final int mZoneId;
324         private final int mConfigId;
325         private boolean mIsConfigActive;
326         private boolean mIsConfigSelected;
327         private boolean mIsDefault;
328         private List<CarVolumeGroupInfo> mConfigVolumeGroups = new ArrayList<>();
329 
330         private long mBuilderFieldsSet;
331 
Builder(@onNull String name, int zoneId, int configId)332         public Builder(@NonNull String name, int zoneId, int configId) {
333             mName = name;
334             mZoneId = zoneId;
335             mConfigId = configId;
336         }
337 
Builder(CarAudioZoneConfigInfo info)338         public Builder(CarAudioZoneConfigInfo info) {
339             this(info.mName, info.mZoneId, info.mConfigId);
340             mIsConfigActive = info.mIsConfigActive;
341             mIsConfigSelected = info.mIsConfigSelected;
342             mIsDefault = info.mIsDefault;
343             mConfigVolumeGroups.addAll(info.mConfigVolumeGroups);
344         }
345 
346         /**
347          * Sets the configurations volume groups
348          *
349          * @param configVolumeGroups volume groups to sent
350          */
setConfigVolumeGroups(List<CarVolumeGroupInfo> configVolumeGroups)351         public Builder setConfigVolumeGroups(List<CarVolumeGroupInfo> configVolumeGroups) {
352             mConfigVolumeGroups = Objects.requireNonNull(configVolumeGroups,
353                     "Config volume groups can not be null");
354             return this;
355         }
356 
357         /**
358          * Sets whether the configuration is active
359          *
360          * @param isActive active state of the configuration, {@code true} for active,
361          * {@code false} otherwise.
362          */
setIsActive(boolean isActive)363         public Builder setIsActive(boolean isActive) {
364             mIsConfigActive = isActive;
365             return this;
366         }
367 
368         /**
369          * Sets whether the configuration is currently selected
370          *
371          * @param isSelected selected state of the configuration, {@code true} for selected,
372          * {@code false} otherwise.
373          */
setIsSelected(boolean isSelected)374         public Builder setIsSelected(boolean isSelected) {
375             mIsConfigSelected = isSelected;
376             return this;
377         }
378 
379         /**
380          * Sets whether the configuration is the default configuration
381          *
382          * @param isDefault default status of the configuration, {@code true} for default,
383          * {@code false} otherwise.
384          */
setIsDefault(boolean isDefault)385         public Builder setIsDefault(boolean isDefault) {
386             mIsDefault = isDefault;
387             return this;
388         }
389 
390         /**
391          * Builds the instance
392          *
393          * @return the constructed volume group info
394          */
build()395         public CarAudioZoneConfigInfo build() throws IllegalStateException {
396             checkNotUsed();
397             mBuilderFieldsSet |= IS_USED_FIELD_SET;
398             return new CarAudioZoneConfigInfo(mName, mConfigVolumeGroups, mZoneId, mConfigId,
399                     mIsConfigActive, mIsConfigSelected, mIsDefault);
400         }
401 
checkNotUsed()402         private void checkNotUsed() throws IllegalStateException {
403             if ((mBuilderFieldsSet & IS_USED_FIELD_SET) != 0) {
404                 throw new IllegalStateException(
405                         "This Builder should not be reused. Use a new Builder instance instead");
406             }
407         }
408     }
409 }
410