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