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