1 /** 2 * Copyright (C) 2017 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 com.android.server.broadcastradio.hal2; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.hardware.broadcastradio.V2_0.AmFmBandRange; 22 import android.hardware.broadcastradio.V2_0.AmFmRegionConfig; 23 import android.hardware.broadcastradio.V2_0.Announcement; 24 import android.hardware.broadcastradio.V2_0.DabTableEntry; 25 import android.hardware.broadcastradio.V2_0.IdentifierType; 26 import android.hardware.broadcastradio.V2_0.Metadata; 27 import android.hardware.broadcastradio.V2_0.MetadataKey; 28 import android.hardware.broadcastradio.V2_0.ProgramFilter; 29 import android.hardware.broadcastradio.V2_0.ProgramIdentifier; 30 import android.hardware.broadcastradio.V2_0.ProgramInfo; 31 import android.hardware.broadcastradio.V2_0.ProgramListChunk; 32 import android.hardware.broadcastradio.V2_0.Properties; 33 import android.hardware.broadcastradio.V2_0.Result; 34 import android.hardware.broadcastradio.V2_0.VendorKeyValue; 35 import android.hardware.radio.ProgramList; 36 import android.hardware.radio.ProgramSelector; 37 import android.hardware.radio.RadioManager; 38 import android.hardware.radio.RadioMetadata; 39 import android.hardware.radio.RadioTuner; 40 import android.os.ParcelableException; 41 import android.util.Slog; 42 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.Collection; 46 import java.util.Collections; 47 import java.util.HashMap; 48 import java.util.HashSet; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.Objects; 52 import java.util.Set; 53 import java.util.stream.Collectors; 54 55 class Convert { 56 57 private static final String TAG = "BcRadio2Srv.convert"; 58 Convert()59 private Convert() { 60 throw new UnsupportedOperationException("Convert class is noninstantiable"); 61 } 62 throwOnError(String action, int result)63 static void throwOnError(String action, int result) { 64 String errorString = action + ": " + Result.toString(result); 65 switch (result) { 66 case Result.OK: 67 return; 68 case Result.UNKNOWN_ERROR: 69 case Result.INTERNAL_ERROR: 70 case Result.TIMEOUT: 71 throw new ParcelableException(new RuntimeException(errorString)); 72 case Result.INVALID_ARGUMENTS: 73 throw new IllegalArgumentException(errorString); 74 case Result.INVALID_STATE: 75 throw new IllegalStateException(errorString); 76 case Result.NOT_SUPPORTED: 77 throw new UnsupportedOperationException(errorString); 78 default: 79 throw new ParcelableException(new RuntimeException( 80 action + ": unknown error (" + result + ")")); 81 } 82 } 83 84 @RadioTuner.TunerResultType halResultToTunerResult(int result)85 static int halResultToTunerResult(int result) { 86 switch (result) { 87 case Result.OK: 88 return RadioTuner.TUNER_RESULT_OK; 89 case Result.INTERNAL_ERROR: 90 return RadioTuner.TUNER_RESULT_INTERNAL_ERROR; 91 case Result.INVALID_ARGUMENTS: 92 return RadioTuner.TUNER_RESULT_INVALID_ARGUMENTS; 93 case Result.INVALID_STATE: 94 return RadioTuner.TUNER_RESULT_INVALID_STATE; 95 case Result.NOT_SUPPORTED: 96 return RadioTuner.TUNER_RESULT_NOT_SUPPORTED; 97 case Result.TIMEOUT: 98 return RadioTuner.TUNER_RESULT_TIMEOUT; 99 case Result.UNKNOWN_ERROR: 100 default: 101 return RadioTuner.TUNER_RESULT_UNKNOWN_ERROR; 102 } 103 } 104 105 static @NonNull ArrayList<VendorKeyValue> vendorInfoToHal(@ullable Map<String, String> info)106 vendorInfoToHal(@Nullable Map<String, String> info) { 107 if (info == null) return new ArrayList<>(); 108 109 ArrayList<VendorKeyValue> list = new ArrayList<>(); 110 for (Map.Entry<String, String> entry : info.entrySet()) { 111 VendorKeyValue elem = new VendorKeyValue(); 112 elem.key = entry.getKey(); 113 elem.value = entry.getValue(); 114 if (elem.key == null || elem.value == null) { 115 Slog.w(TAG, "VendorKeyValue contains null pointers"); 116 continue; 117 } 118 list.add(elem); 119 } 120 121 return list; 122 } 123 124 static @NonNull Map<String, String> vendorInfoFromHal(@ullable List<VendorKeyValue> info)125 vendorInfoFromHal(@Nullable List<VendorKeyValue> info) { 126 if (info == null) return Collections.emptyMap(); 127 128 Map<String, String> map = new HashMap<>(); 129 for (VendorKeyValue kvp : info) { 130 if (kvp.key == null || kvp.value == null) { 131 Slog.w(TAG, "VendorKeyValue contains null pointers"); 132 continue; 133 } 134 map.put(kvp.key, kvp.value); 135 } 136 137 return map; 138 } 139 identifierTypeToProgramType( @rogramSelector.IdentifierType int idType)140 private static @ProgramSelector.ProgramType int identifierTypeToProgramType( 141 @ProgramSelector.IdentifierType int idType) { 142 switch (idType) { 143 case ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY: 144 case ProgramSelector.IDENTIFIER_TYPE_RDS_PI: 145 // TODO(b/69958423): verify AM/FM with frequency range 146 return ProgramSelector.PROGRAM_TYPE_FM; 147 case ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT: 148 // TODO(b/69958423): verify AM/FM with frequency range 149 return ProgramSelector.PROGRAM_TYPE_FM_HD; 150 case ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC: 151 case ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE: 152 case ProgramSelector.IDENTIFIER_TYPE_DAB_SCID: 153 case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY: 154 case ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT: 155 return ProgramSelector.PROGRAM_TYPE_DAB; 156 case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID: 157 case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY: 158 return ProgramSelector.PROGRAM_TYPE_DRMO; 159 case ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID: 160 case ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL: 161 return ProgramSelector.PROGRAM_TYPE_SXM; 162 } 163 if (idType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_START 164 && idType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_END) { 165 return idType; 166 } 167 return ProgramSelector.PROGRAM_TYPE_INVALID; 168 } 169 170 private static @NonNull int[] identifierTypesToProgramTypes(@onNull int[] idTypes)171 identifierTypesToProgramTypes(@NonNull int[] idTypes) { 172 Set<Integer> pTypes = new HashSet<>(); 173 174 for (int idType : idTypes) { 175 int pType = identifierTypeToProgramType(idType); 176 177 if (pType == ProgramSelector.PROGRAM_TYPE_INVALID) continue; 178 179 pTypes.add(pType); 180 if (pType == ProgramSelector.PROGRAM_TYPE_FM) { 181 // TODO(b/69958423): verify AM/FM with region info 182 pTypes.add(ProgramSelector.PROGRAM_TYPE_AM); 183 } 184 if (pType == ProgramSelector.PROGRAM_TYPE_FM_HD) { 185 // TODO(b/69958423): verify AM/FM with region info 186 pTypes.add(ProgramSelector.PROGRAM_TYPE_AM_HD); 187 } 188 } 189 190 return pTypes.stream().mapToInt(Integer::intValue).toArray(); 191 } 192 193 private static @NonNull RadioManager.BandDescriptor[] amfmConfigToBands(@ullable AmFmRegionConfig config)194 amfmConfigToBands(@Nullable AmFmRegionConfig config) { 195 if (config == null) return new RadioManager.BandDescriptor[0]; 196 197 int len = config.ranges.size(); 198 List<RadioManager.BandDescriptor> bands = new ArrayList<>(len); 199 200 // Just a placeholder value. 201 int region = RadioManager.REGION_ITU_1; 202 203 for (AmFmBandRange range : config.ranges) { 204 FrequencyBand bandType = Utils.getBand(range.lowerBound); 205 if (bandType == FrequencyBand.UNKNOWN) { 206 Slog.e(TAG, "Unknown frequency band at " + range.lowerBound + "kHz"); 207 continue; 208 } 209 if (bandType == FrequencyBand.FM) { 210 bands.add(new RadioManager.FmBandDescriptor(region, RadioManager.BAND_FM, 211 range.lowerBound, range.upperBound, range.spacing, 212 213 // TODO(b/69958777): stereo, rds, ta, af, ea 214 true, true, true, true, true 215 )); 216 } else { // AM 217 bands.add(new RadioManager.AmBandDescriptor(region, RadioManager.BAND_AM, 218 range.lowerBound, range.upperBound, range.spacing, 219 220 // TODO(b/69958777): stereo 221 true 222 )); 223 } 224 } 225 226 return bands.toArray(new RadioManager.BandDescriptor[bands.size()]); 227 } 228 dabConfigFromHal( @ullable List<DabTableEntry> config)229 private static @Nullable Map<String, Integer> dabConfigFromHal( 230 @Nullable List<DabTableEntry> config) { 231 if (config == null) return null; 232 return config.stream().collect(Collectors.toMap(e -> e.label, e -> e.frequency)); 233 } 234 235 static @NonNull RadioManager.ModuleProperties propertiesFromHal(int id, @NonNull String serviceName, @NonNull Properties prop, @Nullable AmFmRegionConfig amfmConfig, @Nullable List<DabTableEntry> dabConfig)236 propertiesFromHal(int id, @NonNull String serviceName, @NonNull Properties prop, 237 @Nullable AmFmRegionConfig amfmConfig, @Nullable List<DabTableEntry> dabConfig) { 238 Objects.requireNonNull(serviceName); 239 Objects.requireNonNull(prop); 240 241 int[] supportedIdentifierTypes = prop.supportedIdentifierTypes.stream(). 242 mapToInt(Integer::intValue).toArray(); 243 int[] supportedProgramTypes = identifierTypesToProgramTypes(supportedIdentifierTypes); 244 245 return new RadioManager.ModuleProperties( 246 id, 247 serviceName, 248 249 // There is no Class concept in HAL 2.0. 250 RadioManager.CLASS_AM_FM, 251 252 prop.maker, 253 prop.product, 254 prop.version, 255 prop.serial, 256 257 /* HAL 2.0 only supports single tuner and audio source per 258 * HAL implementation instance. */ 259 1, // numTuners 260 1, // numAudioSources 261 false, // isInitializationRequired 262 false, // isCaptureSupported 263 264 amfmConfigToBands(amfmConfig), 265 true, // isBgScanSupported is deprecated 266 supportedProgramTypes, 267 supportedIdentifierTypes, 268 dabConfigFromHal(dabConfig), 269 vendorInfoFromHal(prop.vendorInfo) 270 ); 271 } 272 programIdentifierToHal(@onNull ProgramIdentifier hwId, @NonNull ProgramSelector.Identifier id)273 static void programIdentifierToHal(@NonNull ProgramIdentifier hwId, 274 @NonNull ProgramSelector.Identifier id) { 275 hwId.type = id.getType(); 276 hwId.value = id.getValue(); 277 } 278 programIdentifierToHal( @onNull ProgramSelector.Identifier id)279 static @NonNull ProgramIdentifier programIdentifierToHal( 280 @NonNull ProgramSelector.Identifier id) { 281 ProgramIdentifier hwId = new ProgramIdentifier(); 282 programIdentifierToHal(hwId, id); 283 return hwId; 284 } 285 programIdentifierFromHal( @onNull ProgramIdentifier id)286 static @Nullable ProgramSelector.Identifier programIdentifierFromHal( 287 @NonNull ProgramIdentifier id) { 288 if (id.type == IdentifierType.INVALID) return null; 289 return new ProgramSelector.Identifier(id.type, id.value); 290 } 291 programSelectorToHal( @onNull ProgramSelector sel)292 static @NonNull android.hardware.broadcastradio.V2_0.ProgramSelector programSelectorToHal( 293 @NonNull ProgramSelector sel) { 294 android.hardware.broadcastradio.V2_0.ProgramSelector hwSel = 295 new android.hardware.broadcastradio.V2_0.ProgramSelector(); 296 297 programIdentifierToHal(hwSel.primaryId, sel.getPrimaryId()); 298 Arrays.stream(sel.getSecondaryIds()).map(Convert::programIdentifierToHal). 299 forEachOrdered(hwSel.secondaryIds::add); 300 301 return hwSel; 302 } 303 isEmpty( @onNull android.hardware.broadcastradio.V2_0.ProgramSelector sel)304 private static boolean isEmpty( 305 @NonNull android.hardware.broadcastradio.V2_0.ProgramSelector sel) { 306 if (sel.primaryId.type != 0) return false; 307 if (sel.primaryId.value != 0) return false; 308 if (sel.secondaryIds.size() != 0) return false; 309 return true; 310 } 311 programSelectorFromHal( @onNull android.hardware.broadcastradio.V2_0.ProgramSelector sel)312 static @Nullable ProgramSelector programSelectorFromHal( 313 @NonNull android.hardware.broadcastradio.V2_0.ProgramSelector sel) { 314 if (isEmpty(sel)) return null; 315 316 ProgramSelector.Identifier[] secondaryIds = sel.secondaryIds.stream(). 317 map(Convert::programIdentifierFromHal).map(Objects::requireNonNull). 318 toArray(ProgramSelector.Identifier[]::new); 319 320 return new ProgramSelector( 321 identifierTypeToProgramType(sel.primaryId.type), 322 Objects.requireNonNull(programIdentifierFromHal(sel.primaryId)), 323 secondaryIds, null); 324 } 325 326 private enum MetadataType { 327 INT, STRING 328 } 329 330 private static class MetadataDef { 331 private MetadataType type; 332 private String key; MetadataDef(MetadataType type, String key)333 private MetadataDef(MetadataType type, String key) { 334 this.type = type; 335 this.key = key; 336 } 337 } 338 339 private static final Map<Integer, MetadataDef> metadataKeys; 340 static { 341 metadataKeys = new HashMap<>(); metadataKeys.put(MetadataKey.RDS_PS, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_RDS_PS))342 metadataKeys.put(MetadataKey.RDS_PS, new MetadataDef( 343 MetadataType.STRING, RadioMetadata.METADATA_KEY_RDS_PS)); metadataKeys.put(MetadataKey.RDS_PTY, new MetadataDef( MetadataType.INT, RadioMetadata.METADATA_KEY_RDS_PTY))344 metadataKeys.put(MetadataKey.RDS_PTY, new MetadataDef( 345 MetadataType.INT, RadioMetadata.METADATA_KEY_RDS_PTY)); metadataKeys.put(MetadataKey.RBDS_PTY, new MetadataDef( MetadataType.INT, RadioMetadata.METADATA_KEY_RBDS_PTY))346 metadataKeys.put(MetadataKey.RBDS_PTY, new MetadataDef( 347 MetadataType.INT, RadioMetadata.METADATA_KEY_RBDS_PTY)); metadataKeys.put(MetadataKey.RDS_RT, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_RDS_RT))348 metadataKeys.put(MetadataKey.RDS_RT, new MetadataDef( 349 MetadataType.STRING, RadioMetadata.METADATA_KEY_RDS_RT)); metadataKeys.put(MetadataKey.SONG_TITLE, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_TITLE))350 metadataKeys.put(MetadataKey.SONG_TITLE, new MetadataDef( 351 MetadataType.STRING, RadioMetadata.METADATA_KEY_TITLE)); metadataKeys.put(MetadataKey.SONG_ARTIST, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_ARTIST))352 metadataKeys.put(MetadataKey.SONG_ARTIST, new MetadataDef( 353 MetadataType.STRING, RadioMetadata.METADATA_KEY_ARTIST)); metadataKeys.put(MetadataKey.SONG_ALBUM, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_ALBUM))354 metadataKeys.put(MetadataKey.SONG_ALBUM, new MetadataDef( 355 MetadataType.STRING, RadioMetadata.METADATA_KEY_ALBUM)); metadataKeys.put(MetadataKey.STATION_ICON, new MetadataDef( MetadataType.INT, RadioMetadata.METADATA_KEY_ICON))356 metadataKeys.put(MetadataKey.STATION_ICON, new MetadataDef( 357 MetadataType.INT, RadioMetadata.METADATA_KEY_ICON)); metadataKeys.put(MetadataKey.ALBUM_ART, new MetadataDef( MetadataType.INT, RadioMetadata.METADATA_KEY_ART))358 metadataKeys.put(MetadataKey.ALBUM_ART, new MetadataDef( 359 MetadataType.INT, RadioMetadata.METADATA_KEY_ART)); metadataKeys.put(MetadataKey.PROGRAM_NAME, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_PROGRAM_NAME))360 metadataKeys.put(MetadataKey.PROGRAM_NAME, new MetadataDef( 361 MetadataType.STRING, RadioMetadata.METADATA_KEY_PROGRAM_NAME)); metadataKeys.put(MetadataKey.DAB_ENSEMBLE_NAME, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME))362 metadataKeys.put(MetadataKey.DAB_ENSEMBLE_NAME, new MetadataDef( 363 MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME)); metadataKeys.put(MetadataKey.DAB_ENSEMBLE_NAME_SHORT, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME_SHORT))364 metadataKeys.put(MetadataKey.DAB_ENSEMBLE_NAME_SHORT, new MetadataDef( 365 MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME_SHORT)); metadataKeys.put(MetadataKey.DAB_SERVICE_NAME, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME))366 metadataKeys.put(MetadataKey.DAB_SERVICE_NAME, new MetadataDef( 367 MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME)); metadataKeys.put(MetadataKey.DAB_SERVICE_NAME_SHORT, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME_SHORT))368 metadataKeys.put(MetadataKey.DAB_SERVICE_NAME_SHORT, new MetadataDef( 369 MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME_SHORT)); metadataKeys.put(MetadataKey.DAB_COMPONENT_NAME, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME))370 metadataKeys.put(MetadataKey.DAB_COMPONENT_NAME, new MetadataDef( 371 MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME)); metadataKeys.put(MetadataKey.DAB_COMPONENT_NAME_SHORT, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME_SHORT))372 metadataKeys.put(MetadataKey.DAB_COMPONENT_NAME_SHORT, new MetadataDef( 373 MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME_SHORT)); 374 } 375 metadataFromHal(@onNull ArrayList<Metadata> meta)376 private static @NonNull RadioMetadata metadataFromHal(@NonNull ArrayList<Metadata> meta) { 377 RadioMetadata.Builder builder = new RadioMetadata.Builder(); 378 379 for (Metadata entry : meta) { 380 MetadataDef keyDef = metadataKeys.get(entry.key); 381 if (keyDef == null) { 382 Slog.i(TAG, "Ignored unknown metadata entry: " + MetadataKey.toString(entry.key)); 383 continue; 384 } 385 if (keyDef.type == MetadataType.STRING) { 386 builder.putString(keyDef.key, entry.stringValue); 387 } else { // MetadataType.INT 388 /* Current java API use 32-bit values for int metadata, 389 * but we might change it in the future */ 390 builder.putInt(keyDef.key, (int)entry.intValue); 391 } 392 } 393 394 return builder.build(); 395 } 396 programInfoFromHal(@onNull ProgramInfo info)397 static @NonNull RadioManager.ProgramInfo programInfoFromHal(@NonNull ProgramInfo info) { 398 Collection<ProgramSelector.Identifier> relatedContent = info.relatedContent.stream(). 399 map(id -> Objects.requireNonNull(programIdentifierFromHal(id))). 400 collect(Collectors.toList()); 401 402 return new RadioManager.ProgramInfo( 403 Objects.requireNonNull(programSelectorFromHal(info.selector)), 404 programIdentifierFromHal(info.logicallyTunedTo), 405 programIdentifierFromHal(info.physicallyTunedTo), 406 relatedContent, 407 info.infoFlags, 408 info.signalQuality, 409 metadataFromHal(info.metadata), 410 vendorInfoFromHal(info.vendorInfo) 411 ); 412 } 413 programFilterToHal(@ullable ProgramList.Filter filter)414 static @NonNull ProgramFilter programFilterToHal(@Nullable ProgramList.Filter filter) { 415 if (filter == null) filter = new ProgramList.Filter(); 416 417 ProgramFilter hwFilter = new ProgramFilter(); 418 419 filter.getIdentifierTypes().stream().forEachOrdered(hwFilter.identifierTypes::add); 420 filter.getIdentifiers().stream().forEachOrdered( 421 id -> hwFilter.identifiers.add(programIdentifierToHal(id))); 422 hwFilter.includeCategories = filter.areCategoriesIncluded(); 423 hwFilter.excludeModifications = filter.areModificationsExcluded(); 424 425 return hwFilter; 426 } 427 programListChunkFromHal(@onNull ProgramListChunk chunk)428 static @NonNull ProgramList.Chunk programListChunkFromHal(@NonNull ProgramListChunk chunk) { 429 Set<RadioManager.ProgramInfo> modified = chunk.modified.stream(). 430 map(info -> programInfoFromHal(info)).collect(Collectors.toSet()); 431 Set<ProgramSelector.Identifier> removed = chunk.removed.stream(). 432 map(id -> Objects.requireNonNull(programIdentifierFromHal(id))). 433 collect(Collectors.toSet()); 434 435 return new ProgramList.Chunk(chunk.purge, chunk.complete, modified, removed); 436 } 437 announcementFromHal( @onNull Announcement hwAnnouncement)438 public static @NonNull android.hardware.radio.Announcement announcementFromHal( 439 @NonNull Announcement hwAnnouncement) { 440 return new android.hardware.radio.Announcement( 441 Objects.requireNonNull(programSelectorFromHal(hwAnnouncement.selector)), 442 hwAnnouncement.type, 443 vendorInfoFromHal(hwAnnouncement.vendorInfo) 444 ); 445 } 446 listToArrayList(@ullable List<T> list)447 static <T> @Nullable ArrayList<T> listToArrayList(@Nullable List<T> list) { 448 if (list == null) return null; 449 if (list instanceof ArrayList) return (ArrayList) list; 450 return new ArrayList<>(list); 451 } 452 } 453