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.annotation.NonNull; 19 import android.car.media.CarAudioManager; 20 import android.media.AudioDeviceAttributes; 21 import android.media.AudioDeviceInfo; 22 import android.text.TextUtils; 23 import android.util.SparseIntArray; 24 import android.util.Xml; 25 26 import com.android.car.audio.CarAudioContext.AudioContext; 27 import com.android.internal.util.Preconditions; 28 29 import org.xmlpull.v1.XmlPullParser; 30 import org.xmlpull.v1.XmlPullParserException; 31 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.Comparator; 37 import java.util.HashMap; 38 import java.util.HashSet; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Objects; 42 import java.util.Set; 43 import java.util.stream.Collectors; 44 45 /** 46 * A helper class loads all audio zones from the configuration XML file. 47 */ 48 /* package */ class CarAudioZonesHelper { 49 private static final String NAMESPACE = null; 50 private static final String TAG_ROOT = "carAudioConfiguration"; 51 private static final String TAG_AUDIO_ZONES = "zones"; 52 private static final String TAG_AUDIO_ZONE = "zone"; 53 private static final String TAG_VOLUME_GROUPS = "volumeGroups"; 54 private static final String TAG_VOLUME_GROUP = "group"; 55 private static final String TAG_AUDIO_DEVICE = "device"; 56 private static final String TAG_CONTEXT = "context"; 57 private static final String ATTR_VERSION = "version"; 58 private static final String ATTR_IS_PRIMARY = "isPrimary"; 59 private static final String ATTR_ZONE_NAME = "name"; 60 private static final String ATTR_DEVICE_ADDRESS = "address"; 61 private static final String ATTR_CONTEXT_NAME = "context"; 62 private static final String ATTR_ZONE_ID = "audioZoneId"; 63 private static final String ATTR_OCCUPANT_ZONE_ID = "occupantZoneId"; 64 private static final String TAG_INPUT_DEVICES = "inputDevices"; 65 private static final String TAG_INPUT_DEVICE = "inputDevice"; 66 private static final int INVALID_VERSION = -1; 67 private static final int SUPPORTED_VERSION_1 = 1; 68 private static final int SUPPORTED_VERSION_2 = 2; 69 private static final SparseIntArray SUPPORTED_VERSIONS; 70 71 72 private static final Map<String, Integer> CONTEXT_NAME_MAP; 73 74 static { 75 CONTEXT_NAME_MAP = new HashMap<>(CarAudioContext.CONTEXTS.length); 76 CONTEXT_NAME_MAP.put("music", CarAudioContext.MUSIC); 77 CONTEXT_NAME_MAP.put("navigation", CarAudioContext.NAVIGATION); 78 CONTEXT_NAME_MAP.put("voice_command", CarAudioContext.VOICE_COMMAND); 79 CONTEXT_NAME_MAP.put("call_ring", CarAudioContext.CALL_RING); 80 CONTEXT_NAME_MAP.put("call", CarAudioContext.CALL); 81 CONTEXT_NAME_MAP.put("alarm", CarAudioContext.ALARM); 82 CONTEXT_NAME_MAP.put("notification", CarAudioContext.NOTIFICATION); 83 CONTEXT_NAME_MAP.put("system_sound", CarAudioContext.SYSTEM_SOUND); 84 CONTEXT_NAME_MAP.put("emergency", CarAudioContext.EMERGENCY); 85 CONTEXT_NAME_MAP.put("safety", CarAudioContext.SAFETY); 86 CONTEXT_NAME_MAP.put("vehicle_status", CarAudioContext.VEHICLE_STATUS); 87 CONTEXT_NAME_MAP.put("announcement", CarAudioContext.ANNOUNCEMENT); 88 89 SUPPORTED_VERSIONS = new SparseIntArray(2); SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_1, SUPPORTED_VERSION_1)90 SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_1, SUPPORTED_VERSION_1); SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_2, SUPPORTED_VERSION_2)91 SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_2, SUPPORTED_VERSION_2); 92 } 93 94 // Same contexts as defined in android.hardware.automotive.audiocontrol.V1_0.ContextNumber 95 static final int[] LEGACY_CONTEXTS = new int[]{ 96 CarAudioContext.MUSIC, 97 CarAudioContext.NAVIGATION, 98 CarAudioContext.VOICE_COMMAND, 99 CarAudioContext.CALL_RING, 100 CarAudioContext.CALL, 101 CarAudioContext.ALARM, 102 CarAudioContext.NOTIFICATION, 103 CarAudioContext.SYSTEM_SOUND 104 }; 105 isLegacyContext(@udioContext int audioContext)106 private static boolean isLegacyContext(@AudioContext int audioContext) { 107 return Arrays.binarySearch(LEGACY_CONTEXTS, audioContext) >= 0; 108 } 109 110 private static final List<Integer> NON_LEGACY_CONTEXTS = new ArrayList<>( 111 CarAudioContext.CONTEXTS.length - LEGACY_CONTEXTS.length); 112 113 static { 114 for (@AudioContext int audioContext : CarAudioContext.CONTEXTS) { 115 if (!isLegacyContext(audioContext)) { 116 NON_LEGACY_CONTEXTS.add(audioContext); 117 } 118 } 119 } 120 bindNonLegacyContexts(CarVolumeGroup group, CarAudioDeviceInfo info)121 static void bindNonLegacyContexts(CarVolumeGroup group, CarAudioDeviceInfo info) { 122 for (@AudioContext int audioContext : NON_LEGACY_CONTEXTS) { 123 group.bind(audioContext, info); 124 } 125 } 126 127 private final CarAudioSettings mCarAudioSettings; 128 private final Map<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo; 129 private final Map<String, AudioDeviceInfo> mAddressToInputAudioDeviceInfo; 130 private final InputStream mInputStream; 131 private final SparseIntArray mZoneIdToOccupantZoneIdMapping; 132 private final Set<Integer> mAudioZoneIds; 133 private final Set<String> mInputAudioDevices; 134 135 private boolean mHasPrimaryZone; 136 private int mNextSecondaryZoneId; 137 private int mCurrentVersion; 138 139 /** 140 * <p><b>Note: <b/> CarAudioZonesHelper is expected to be used from a single thread. This 141 * should be the same thread that originally called new CarAudioZonesHelper. 142 */ CarAudioZonesHelper(@onNull CarAudioSettings carAudioSettings, @NonNull InputStream inputStream, @NonNull List<CarAudioDeviceInfo> carAudioDeviceInfos, @NonNull AudioDeviceInfo[] inputDeviceInfo)143 CarAudioZonesHelper(@NonNull CarAudioSettings carAudioSettings, 144 @NonNull InputStream inputStream, 145 @NonNull List<CarAudioDeviceInfo> carAudioDeviceInfos, 146 @NonNull AudioDeviceInfo[] inputDeviceInfo) { 147 mCarAudioSettings = Objects.requireNonNull(carAudioSettings); 148 mInputStream = Objects.requireNonNull(inputStream); 149 Objects.requireNonNull(carAudioDeviceInfos); 150 Objects.requireNonNull(inputDeviceInfo); 151 mAddressToCarAudioDeviceInfo = CarAudioZonesHelper.generateAddressToInfoMap( 152 carAudioDeviceInfos); 153 mAddressToInputAudioDeviceInfo = 154 CarAudioZonesHelper.generateAddressToInputAudioDeviceInfoMap(inputDeviceInfo); 155 mNextSecondaryZoneId = CarAudioManager.PRIMARY_AUDIO_ZONE + 1; 156 mZoneIdToOccupantZoneIdMapping = new SparseIntArray(); 157 mAudioZoneIds = new HashSet<>(); 158 mInputAudioDevices = new HashSet<>(); 159 } 160 getCarAudioZoneIdToOccupantZoneIdMapping()161 SparseIntArray getCarAudioZoneIdToOccupantZoneIdMapping() { 162 return mZoneIdToOccupantZoneIdMapping; 163 } 164 165 // TODO: refactor this method to return List<CarAudioZone> loadAudioZones()166 CarAudioZone[] loadAudioZones() throws IOException, XmlPullParserException { 167 List<CarAudioZone> carAudioZones = new ArrayList<>(); 168 parseCarAudioZones(carAudioZones, mInputStream); 169 return carAudioZones.toArray(new CarAudioZone[0]); 170 } 171 generateAddressToInfoMap( List<CarAudioDeviceInfo> carAudioDeviceInfos)172 private static Map<String, CarAudioDeviceInfo> generateAddressToInfoMap( 173 List<CarAudioDeviceInfo> carAudioDeviceInfos) { 174 return carAudioDeviceInfos.stream() 175 .filter(info -> !TextUtils.isEmpty(info.getAddress())) 176 .collect(Collectors.toMap(CarAudioDeviceInfo::getAddress, info -> info)); 177 } 178 generateAddressToInputAudioDeviceInfoMap( @onNull AudioDeviceInfo[] inputAudioDeviceInfos)179 private static Map<String, AudioDeviceInfo> generateAddressToInputAudioDeviceInfoMap( 180 @NonNull AudioDeviceInfo[] inputAudioDeviceInfos) { 181 HashMap<String, AudioDeviceInfo> deviceAddressToInputDeviceMap = 182 new HashMap<>(inputAudioDeviceInfos.length); 183 for (int i = 0; i < inputAudioDeviceInfos.length; ++i) { 184 AudioDeviceInfo device = inputAudioDeviceInfos[i]; 185 if (device.isSource()) { 186 deviceAddressToInputDeviceMap.put(device.getAddress(), device); 187 } 188 } 189 return deviceAddressToInputDeviceMap; 190 } 191 parseCarAudioZones(List<CarAudioZone> carAudioZones, InputStream stream)192 private void parseCarAudioZones(List<CarAudioZone> carAudioZones, InputStream stream) 193 throws XmlPullParserException, IOException { 194 final XmlPullParser parser = Xml.newPullParser(); 195 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, NAMESPACE != null); 196 parser.setInput(stream, null); 197 198 // Ensure <carAudioConfiguration> is the root 199 parser.nextTag(); 200 parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_ROOT); 201 202 // Version check 203 final int versionNumber = Integer.parseInt( 204 parser.getAttributeValue(NAMESPACE, ATTR_VERSION)); 205 206 if (SUPPORTED_VERSIONS.get(versionNumber, INVALID_VERSION) == INVALID_VERSION) { 207 throw new IllegalArgumentException("Latest Supported version:" 208 + SUPPORTED_VERSION_2 + " , got version:" + versionNumber); 209 } 210 211 mCurrentVersion = versionNumber; 212 213 // Get all zones configured under <zones> tag 214 while (parser.next() != XmlPullParser.END_TAG) { 215 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 216 if (TAG_AUDIO_ZONES.equals(parser.getName())) { 217 parseAudioZones(parser, carAudioZones); 218 } else { 219 skip(parser); 220 } 221 } 222 } 223 parseAudioZones(XmlPullParser parser, List<CarAudioZone> carAudioZones)224 private void parseAudioZones(XmlPullParser parser, List<CarAudioZone> carAudioZones) 225 throws XmlPullParserException, IOException { 226 while (parser.next() != XmlPullParser.END_TAG) { 227 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 228 if (TAG_AUDIO_ZONE.equals(parser.getName())) { 229 carAudioZones.add(parseAudioZone(parser)); 230 } else { 231 skip(parser); 232 } 233 } 234 Preconditions.checkArgument(mHasPrimaryZone, "Requires one primary zone"); 235 carAudioZones.sort(Comparator.comparing(CarAudioZone::getId)); 236 } 237 parseAudioZone(XmlPullParser parser)238 private CarAudioZone parseAudioZone(XmlPullParser parser) 239 throws XmlPullParserException, IOException { 240 final boolean isPrimary = Boolean.parseBoolean( 241 parser.getAttributeValue(NAMESPACE, ATTR_IS_PRIMARY)); 242 if (isPrimary) { 243 Preconditions.checkArgument(!mHasPrimaryZone, "Only one primary zone is allowed"); 244 mHasPrimaryZone = true; 245 } 246 final String zoneName = parser.getAttributeValue(NAMESPACE, ATTR_ZONE_NAME); 247 final int audioZoneId = getZoneId(isPrimary, parser); 248 parseOccupantZoneId(audioZoneId, parser); 249 final CarAudioZone zone = new CarAudioZone(audioZoneId, zoneName); 250 while (parser.next() != XmlPullParser.END_TAG) { 251 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 252 // Expect one <volumeGroups> in one audio zone 253 if (TAG_VOLUME_GROUPS.equals(parser.getName())) { 254 parseVolumeGroups(parser, zone); 255 } else if (TAG_INPUT_DEVICES.equals(parser.getName())) { 256 parseInputAudioDevices(parser, zone); 257 } else { 258 skip(parser); 259 } 260 } 261 return zone; 262 } 263 getZoneId(boolean isPrimary, XmlPullParser parser)264 private int getZoneId(boolean isPrimary, XmlPullParser parser) { 265 String audioZoneIdString = parser.getAttributeValue(NAMESPACE, ATTR_ZONE_ID); 266 if (isVersionOne()) { 267 Preconditions.checkArgument(audioZoneIdString == null, 268 "Invalid audio attribute %s" 269 + ", Please update car audio configurations file " 270 + "to version to 2 to use it.", ATTR_ZONE_ID); 271 return isPrimary ? CarAudioManager.PRIMARY_AUDIO_ZONE 272 : getNextSecondaryZoneId(); 273 } 274 // Primary zone does not need to define it 275 if (isPrimary && audioZoneIdString == null) { 276 return CarAudioManager.PRIMARY_AUDIO_ZONE; 277 } 278 Objects.requireNonNull(audioZoneIdString, () -> 279 "Requires " + ATTR_ZONE_ID + " for all audio zones."); 280 int zoneId = parsePositiveIntAttribute(ATTR_ZONE_ID, audioZoneIdString); 281 //Verify that primary zone id is PRIMARY_AUDIO_ZONE 282 if (isPrimary) { 283 Preconditions.checkArgument(zoneId == CarAudioManager.PRIMARY_AUDIO_ZONE, 284 "Primary zone %s must be %d or it can be left empty.", 285 ATTR_ZONE_ID, CarAudioManager.PRIMARY_AUDIO_ZONE); 286 } else { 287 Preconditions.checkArgument(zoneId != CarAudioManager.PRIMARY_AUDIO_ZONE, 288 "%s can only be %d for primary zone.", 289 ATTR_ZONE_ID, CarAudioManager.PRIMARY_AUDIO_ZONE); 290 } 291 validateAudioZoneIdIsUnique(zoneId); 292 return zoneId; 293 } 294 parseOccupantZoneId(int audioZoneId, XmlPullParser parser)295 private void parseOccupantZoneId(int audioZoneId, XmlPullParser parser) { 296 String occupantZoneIdString = parser.getAttributeValue(NAMESPACE, ATTR_OCCUPANT_ZONE_ID); 297 if (isVersionOne()) { 298 Preconditions.checkArgument(occupantZoneIdString == null, 299 "Invalid audio attribute %s" 300 + ", Please update car audio configurations file " 301 + "to version to 2 to use it.", ATTR_OCCUPANT_ZONE_ID); 302 return; 303 } 304 //Occupant id not required for all zones 305 if (occupantZoneIdString == null) { 306 return; 307 } 308 int occupantZoneId = parsePositiveIntAttribute(ATTR_OCCUPANT_ZONE_ID, occupantZoneIdString); 309 validateOccupantZoneIdIsUnique(occupantZoneId); 310 mZoneIdToOccupantZoneIdMapping.put(audioZoneId, occupantZoneId); 311 } 312 parsePositiveIntAttribute(String attribute, String integerString)313 private int parsePositiveIntAttribute(String attribute, String integerString) { 314 try { 315 return Integer.parseUnsignedInt(integerString); 316 } catch (NumberFormatException | IndexOutOfBoundsException e) { 317 throw new IllegalArgumentException(attribute + " must be a positive integer, but was \"" 318 + integerString + "\" instead.", e); 319 } 320 } 321 parseInputAudioDevices(XmlPullParser parser, CarAudioZone zone)322 private void parseInputAudioDevices(XmlPullParser parser, CarAudioZone zone) 323 throws IOException, XmlPullParserException { 324 if (isVersionOne()) { 325 throw new IllegalStateException( 326 TAG_INPUT_DEVICES + " are not supported in car_audio_configuration.xml version " 327 + SUPPORTED_VERSION_1); 328 } 329 while (parser.next() != XmlPullParser.END_TAG) { 330 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 331 if (TAG_INPUT_DEVICE.equals(parser.getName())) { 332 String audioDeviceAddress = 333 parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS); 334 validateInputAudioDeviceAddress(audioDeviceAddress); 335 AudioDeviceInfo info = mAddressToInputAudioDeviceInfo.get(audioDeviceAddress); 336 Preconditions.checkArgument(info != null, 337 "%s %s of %s does not exist, add input device to" 338 + " audio_policy_configuration.xml.", 339 ATTR_DEVICE_ADDRESS, audioDeviceAddress, TAG_INPUT_DEVICE); 340 zone.addInputAudioDevice(new AudioDeviceAttributes(info)); 341 } 342 skip(parser); 343 } 344 } 345 validateInputAudioDeviceAddress(String audioDeviceAddress)346 private void validateInputAudioDeviceAddress(String audioDeviceAddress) { 347 Objects.requireNonNull(audioDeviceAddress, () -> 348 TAG_INPUT_DEVICE + " " + ATTR_DEVICE_ADDRESS + " attribute must be present."); 349 Preconditions.checkArgument(!audioDeviceAddress.isEmpty(), 350 "%s %s attribute can not be empty.", 351 TAG_INPUT_DEVICE, ATTR_DEVICE_ADDRESS); 352 if (mInputAudioDevices.contains(audioDeviceAddress)) { 353 throw new IllegalArgumentException(TAG_INPUT_DEVICE + " " + audioDeviceAddress 354 + " repeats, " + TAG_INPUT_DEVICES + " can not repeat."); 355 } 356 mInputAudioDevices.add(audioDeviceAddress); 357 } 358 validateOccupantZoneIdIsUnique(int occupantZoneId)359 private void validateOccupantZoneIdIsUnique(int occupantZoneId) { 360 if (mZoneIdToOccupantZoneIdMapping.indexOfValue(occupantZoneId) > -1) { 361 throw new IllegalArgumentException(ATTR_OCCUPANT_ZONE_ID + " " + occupantZoneId 362 + " is already associated with a zone"); 363 } 364 } 365 validateAudioZoneIdIsUnique(int audioZoneId)366 private void validateAudioZoneIdIsUnique(int audioZoneId) { 367 if (mAudioZoneIds.contains(audioZoneId)) { 368 throw new IllegalArgumentException(ATTR_ZONE_ID + " " + audioZoneId 369 + " is already associated with a zone"); 370 } 371 mAudioZoneIds.add(audioZoneId); 372 } 373 parseVolumeGroups(XmlPullParser parser, CarAudioZone zone)374 private void parseVolumeGroups(XmlPullParser parser, CarAudioZone zone) 375 throws XmlPullParserException, IOException { 376 int groupId = 0; 377 while (parser.next() != XmlPullParser.END_TAG) { 378 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 379 if (TAG_VOLUME_GROUP.equals(parser.getName())) { 380 zone.addVolumeGroup(parseVolumeGroup(parser, zone.getId(), groupId)); 381 groupId++; 382 } else { 383 skip(parser); 384 } 385 } 386 } 387 parseVolumeGroup(XmlPullParser parser, int zoneId, int groupId)388 private CarVolumeGroup parseVolumeGroup(XmlPullParser parser, int zoneId, int groupId) 389 throws XmlPullParserException, IOException { 390 CarVolumeGroup group = new CarVolumeGroup(mCarAudioSettings, zoneId, groupId); 391 while (parser.next() != XmlPullParser.END_TAG) { 392 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 393 if (TAG_AUDIO_DEVICE.equals(parser.getName())) { 394 String address = parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS); 395 validateOutputDeviceExist(address); 396 parseVolumeGroupContexts(parser, group, address); 397 } else { 398 skip(parser); 399 } 400 } 401 return group; 402 } 403 validateOutputDeviceExist(String address)404 private void validateOutputDeviceExist(String address) { 405 if (!mAddressToCarAudioDeviceInfo.containsKey(address)) { 406 throw new IllegalStateException(String.format( 407 "Output device address %s does not belong to any configured output device.", 408 address)); 409 } 410 } 411 parseVolumeGroupContexts( XmlPullParser parser, CarVolumeGroup group, String address)412 private void parseVolumeGroupContexts( 413 XmlPullParser parser, CarVolumeGroup group, String address) 414 throws XmlPullParserException, IOException { 415 while (parser.next() != XmlPullParser.END_TAG) { 416 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 417 if (TAG_CONTEXT.equals(parser.getName())) { 418 @AudioContext int carAudioContext = parseCarAudioContext( 419 parser.getAttributeValue(NAMESPACE, ATTR_CONTEXT_NAME)); 420 validateCarAudioContextSupport(carAudioContext); 421 CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address); 422 group.bind(carAudioContext, info); 423 424 // If V1, default new contexts to same device as DEFAULT_AUDIO_USAGE 425 if (isVersionOne() && carAudioContext == CarAudioService.DEFAULT_AUDIO_CONTEXT) { 426 bindNonLegacyContexts(group, info); 427 } 428 } 429 // Always skip to upper level since we're at the lowest. 430 skip(parser); 431 } 432 } 433 isVersionOne()434 private boolean isVersionOne() { 435 return mCurrentVersion == SUPPORTED_VERSION_1; 436 } 437 skip(XmlPullParser parser)438 private void skip(XmlPullParser parser) throws XmlPullParserException, IOException { 439 if (parser.getEventType() != XmlPullParser.START_TAG) { 440 throw new IllegalStateException(); 441 } 442 int depth = 1; 443 while (depth != 0) { 444 switch (parser.next()) { 445 case XmlPullParser.END_TAG: 446 depth--; 447 break; 448 case XmlPullParser.START_TAG: 449 depth++; 450 break; 451 } 452 } 453 } 454 parseCarAudioContext(String context)455 private static @AudioContext int parseCarAudioContext(String context) { 456 return CONTEXT_NAME_MAP.getOrDefault(context.toLowerCase(), CarAudioContext.INVALID); 457 } 458 validateCarAudioContextSupport(@udioContext int audioContext)459 private void validateCarAudioContextSupport(@AudioContext int audioContext) { 460 if (isVersionOne() && NON_LEGACY_CONTEXTS.contains(audioContext)) { 461 throw new IllegalArgumentException(String.format( 462 "Non-legacy audio contexts such as %s are not supported in " 463 + "car_audio_configuration.xml version %d", 464 CarAudioContext.toString(audioContext), SUPPORTED_VERSION_1)); 465 } 466 } 467 getNextSecondaryZoneId()468 private int getNextSecondaryZoneId() { 469 int zoneId = mNextSecondaryZoneId; 470 mNextSecondaryZoneId += 1; 471 return zoneId; 472 } 473 } 474