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