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 
17 package com.android.car.broadcastradio.support.platform;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.hardware.radio.ProgramSelector;
23 import android.hardware.radio.ProgramSelector.Identifier;
24 import android.hardware.radio.RadioManager;
25 import android.net.Uri;
26 import android.util.Log;
27 
28 import java.lang.annotation.Retention;
29 import java.lang.annotation.RetentionPolicy;
30 import java.text.DecimalFormat;
31 import java.util.ArrayList;
32 import java.util.HashMap;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.function.BiConsumer;
36 import java.util.function.BiFunction;
37 
38 /**
39  * Proposed extensions to android.hardware.radio.ProgramSelector.
40  *
41  * They might eventually get pushed to the framework.
42  */
43 public class ProgramSelectorExt {
44     private static final String TAG = "BcRadioApp.pselext";
45 
46     /**
47      * If this is AM/FM channel (or any other technology using different modulations),
48      * don't return modulation part.
49      */
50     public static final int NAME_NO_MODULATION = 1 << 0;
51 
52     /**
53      * Return only modulation part of channel name.
54      *
55      * If this is not a radio technology using modulation, return nothing
56      * (unless combined with other _ONLY flags in the future).
57      *
58      * If this returns non-null string, it's guaranteed that {@link #NAME_NO_MODULATION}
59      * will return the complement of channel name.
60      */
61     public static final int NAME_MODULATION_ONLY = 1 << 1;
62 
63     /**
64      * If the channel name is not human-readable (i.e. DAB SId), radio technology is displayed
65      * instead. This flag prevents that.
66      *
67      * With radio technology fallback, null pointer may still be returned in case of unsupported
68      * radio technologies.
69      */
70     public static final int NAME_NO_PROGRAM_TYPE_FALLBACK = 1 << 2;
71 
72     /**
73      * Flags to control how channel values are converted to string with {@link #getDisplayName}.
74      *
75      * Upper 16 bits are reserved for {@link ProgramInfoExt#NameFlag}.
76      */
77     @IntDef(prefix = { "NAME_" }, flag = true, value = {
78         NAME_NO_MODULATION,
79         NAME_MODULATION_ONLY,
80     })
81     @Retention(RetentionPolicy.SOURCE)
82     public @interface NameFlag {}
83 
84     private static final String URI_SCHEME_BROADCASTRADIO = "broadcastradio";
85     private static final String URI_AUTHORITY_PROGRAM = "program";
86     private static final String URI_VENDOR_PREFIX = "VENDOR_";
87     private static final String URI_HEX_PREFIX = "0x";
88 
89     private static final DecimalFormat FORMAT_FM = new DecimalFormat("###.#");
90 
91     private static final Map<Integer, String> ID_TO_URI = new HashMap<>();
92     private static final Map<String, Integer> URI_TO_ID = new HashMap<>();
93 
94     /**
95      * New proposed constructor for {@link ProgramSelector}.
96      *
97      * As opposed to the current platform API, this one matches more closely simplified HAL 2.0.
98      *
99      * @param primaryId primary program identifier.
100      * @param secondaryIds list of secondary program identifiers.
101      */
newProgramSelector(@onNull Identifier primaryId, @Nullable Identifier[] secondaryIds)102     public static @NonNull ProgramSelector newProgramSelector(@NonNull Identifier primaryId,
103             @Nullable Identifier[] secondaryIds) {
104         return new ProgramSelector(
105                 identifierToProgramType(primaryId),
106                 primaryId, secondaryIds, null);
107     }
108 
109     // when pushed to the framework, remove similar code from HAL 2.0 service
identifierToProgramType( @onNull Identifier primaryId)110     private static @ProgramSelector.ProgramType int identifierToProgramType(
111             @NonNull Identifier primaryId) {
112         int idType = primaryId.getType();
113         switch (idType) {
114             case ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY:
115                 if (isAmFrequency(primaryId.getValue())) {
116                     return ProgramSelector.PROGRAM_TYPE_AM;
117                 } else {
118                     return ProgramSelector.PROGRAM_TYPE_FM;
119                 }
120             case ProgramSelector.IDENTIFIER_TYPE_RDS_PI:
121                 return ProgramSelector.PROGRAM_TYPE_FM;
122             case ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT:
123                 if (isAmFrequency(IdentifierExt.asHdPrimary(primaryId).getFrequency())) {
124                     return ProgramSelector.PROGRAM_TYPE_AM_HD;
125                 } else {
126                     return ProgramSelector.PROGRAM_TYPE_FM_HD;
127                 }
128             case ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC:
129             case ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE:
130             case ProgramSelector.IDENTIFIER_TYPE_DAB_SCID:
131             case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY:
132                 return ProgramSelector.PROGRAM_TYPE_DAB;
133             case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID:
134             case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY:
135                 return ProgramSelector.PROGRAM_TYPE_DRMO;
136             case ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID:
137             case ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL:
138                 return ProgramSelector.PROGRAM_TYPE_SXM;
139         }
140         if (idType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_START
141                 && idType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_END) {
142             return idType;
143         }
144         return ProgramSelector.PROGRAM_TYPE_INVALID;
145     }
146 
147     /**
148      * Checks, if a given AM frequency is roughly valid and in correct unit.
149      *
150      * It does not check the range precisely: it may provide false positives, but not false
151      * negatives. In particular, it may be way off for certain regions.
152      * The main purpose is to avoid passing inproper units, ie. MHz instead of kHz.
153      * It also can be used to check if a given frequency is likely to be used
154      * with AM or FM modulation.
155      *
156      * @param frequencyKhz the frequency in kHz.
157      * @return true, if the frequency is rougly valid.
158      */
isAmFrequency(long frequencyKhz)159     public static boolean isAmFrequency(long frequencyKhz) {
160         return frequencyKhz > 150 && frequencyKhz <= 30000;
161     }
162 
163     /**
164      * Checks, if a given FM frequency is roughly valid and in correct unit.
165      *
166      * It does not check the range precisely: it may provide false positives, but not false
167      * negatives. In particular, it may be way off for certain regions.
168      * The main purpose is to avoid passing inproper units, ie. MHz instead of kHz.
169      * It also can be used to check if a given frequency is likely to be used
170      * with AM or FM modulation.
171      *
172      * @param frequencyKhz the frequency in kHz.
173      * @return true, if the frequency is rougly valid.
174      */
isFmFrequency(long frequencyKhz)175     public static boolean isFmFrequency(long frequencyKhz) {
176         return frequencyKhz > 60000 && frequencyKhz < 110000;
177     }
178 
179     /**
180      * Provides human-readable representation of AM/FM frequency.
181      *
182      * @param frequencyKhz the frequency in kHz.
183      * @param flags flags that affect display format
184      * @return human-readable formatted frequency
185      */
formatAmFmFrequency(long frequencyKhz, @NameFlag int flags)186     public static @Nullable String formatAmFmFrequency(long frequencyKhz, @NameFlag int flags) {
187         String channel;
188         String modulation;
189 
190         if (isAmFrequency(frequencyKhz)) {
191             channel = Long.toString(frequencyKhz);
192             modulation = "AM";
193         } else if (isFmFrequency(frequencyKhz)) {
194             channel = FORMAT_FM.format(frequencyKhz / 1000f);
195             modulation = "FM";
196         } else {
197             Log.w(TAG, "AM/FM frequency out of range: " + frequencyKhz);
198             return null;
199         }
200 
201         if ((flags & NAME_MODULATION_ONLY) != 0) return modulation;
202         if ((flags & NAME_NO_MODULATION) != 0) return channel;
203         return channel + ' ' + modulation;
204     }
205 
206     /**
207      * Builds new ProgramSelector for AM/FM frequency.
208      *
209      * @param frequencyKhz the frequency in kHz.
210      * @return new ProgramSelector object representing given frequency.
211      * @throws IllegalArgumentException if provided frequency is out of bounds.
212      */
createAmFmSelector(long frequencyKhz)213     public static @NonNull ProgramSelector createAmFmSelector(long frequencyKhz) {
214         if (frequencyKhz < 0 || frequencyKhz > Integer.MAX_VALUE) {
215             throw new IllegalArgumentException("illegal frequency value: " + frequencyKhz);
216         }
217         return ProgramSelector.createAmFmSelector(RadioManager.BAND_INVALID, (int) frequencyKhz);
218     }
219 
220     /**
221      * Checks, if {@link ProgramSelector} contains an id of a given type.
222      *
223      * @param sel selector being checked
224      * @param type identifier type to check for
225      * @return true, if sel contains any identifier of a given type
226      */
hasId(@onNull ProgramSelector sel, @ProgramSelector.IdentifierType int type)227     public static boolean hasId(@NonNull ProgramSelector sel,
228             @ProgramSelector.IdentifierType int type) {
229         try {
230             sel.getFirstId(type);
231             return true;
232         } catch (IllegalArgumentException e) {
233             return false;
234         }
235     }
236 
237     /**
238      * Checks, if {@link ProgramSelector} is a AM/FM program.
239      *
240      * @return true, if the primary identifier of a selector belongs to one of the following
241      *         technologies:
242      *          - Analogue AM/FM
243      *          - FM RDS
244      *          - HD Radio AM/FM
245      */
isAmFmProgram(@onNull ProgramSelector sel)246     public static boolean isAmFmProgram(@NonNull ProgramSelector sel) {
247         int priType = sel.getPrimaryId().getType();
248         return priType == ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY
249                 || priType == ProgramSelector.IDENTIFIER_TYPE_RDS_PI
250                 || priType == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT;
251     }
252 
253     /**
254      * Returns a channel name that can be displayed to the user.
255      *
256      * It's implemented only for radio technologies where the channel is meant
257      * to be presented to the user.
258      *
259      * @param sel the program selector
260      * @return Channel name or null, if radio technology doesn't present channel names to the user.
261      */
getDisplayName(@onNull ProgramSelector sel, @NameFlag int flags)262     public static @Nullable String getDisplayName(@NonNull ProgramSelector sel,
263             @NameFlag int flags) {
264         boolean noProgramTypeFallback = (flags & NAME_NO_PROGRAM_TYPE_FALLBACK) != 0;
265 
266         if (isAmFmProgram(sel)) {
267             if (!hasId(sel, ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)) {
268                 if (noProgramTypeFallback) return null;
269                 // if there is no frequency assigned, let's assume it's a malformed RDS selector
270                 return "FM";
271             }
272             long freq = sel.getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY);
273             return formatAmFmFrequency(freq, flags);
274         }
275 
276         if ((flags & NAME_MODULATION_ONLY) != 0) return null;
277 
278         if (sel.getPrimaryId().getType() == ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID
279                 && hasId(sel, ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL)) {
280             return Long.toString(sel.getFirstId(ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL));
281         }
282 
283         if (noProgramTypeFallback) return null;
284 
285         switch (sel.getPrimaryId().getType()) {
286             case ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID:
287                 return "SXM";
288             case ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC:
289                 return "DAB";
290             case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID:
291                 return "DRMO";
292             default:
293                 return null;
294         }
295     }
296 
297     static {
298         BiConsumer<Integer, String> add = (idType, name) -> {
299             ID_TO_URI.put(idType, name);
300             URI_TO_ID.put(name, idType);
301         };
302 
add.accept(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, "AMFM_FREQUENCY")303         add.accept(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, "AMFM_FREQUENCY");
add.accept(ProgramSelector.IDENTIFIER_TYPE_RDS_PI, "RDS_PI")304         add.accept(ProgramSelector.IDENTIFIER_TYPE_RDS_PI, "RDS_PI");
add.accept(ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT, "HD_STATION_ID_EXT")305         add.accept(ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT, "HD_STATION_ID_EXT");
add.accept(ProgramSelector.IDENTIFIER_TYPE_HD_STATION_NAME, "HD_STATION_NAME")306         add.accept(ProgramSelector.IDENTIFIER_TYPE_HD_STATION_NAME, "HD_STATION_NAME");
add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, "DAB_SID_EXT")307         add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, "DAB_SID_EXT");
add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, "DAB_ENSEMBLE")308         add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, "DAB_ENSEMBLE");
add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_SCID, "DAB_SCID")309         add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_SCID, "DAB_SCID");
add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, "DAB_FREQUENCY")310         add.accept(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, "DAB_FREQUENCY");
add.accept(ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID, "DRMO_SERVICE_ID")311         add.accept(ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID, "DRMO_SERVICE_ID");
add.accept(ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY, "DRMO_FREQUENCY")312         add.accept(ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY, "DRMO_FREQUENCY");
add.accept(ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID, "SXM_SERVICE_ID")313         add.accept(ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID, "SXM_SERVICE_ID");
add.accept(ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL, "SXM_CHANNEL")314         add.accept(ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL, "SXM_CHANNEL");
315     }
316 
typeToUri(int identifierType)317     private static @Nullable String typeToUri(int identifierType) {
318         if (identifierType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_START
319                 && identifierType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_END) {
320             int idx = identifierType - ProgramSelector.IDENTIFIER_TYPE_VENDOR_START;
321             return URI_VENDOR_PREFIX + idx;
322         }
323         return ID_TO_URI.get(identifierType);
324     }
325 
uriToType(@ullable String typeUri)326     private static int uriToType(@Nullable String typeUri) {
327         if (typeUri == null) return ProgramSelector.IDENTIFIER_TYPE_INVALID;
328         if (typeUri.startsWith(URI_VENDOR_PREFIX)) {
329             int idx;
330             try {
331                 idx = Integer.parseInt(typeUri.substring(URI_VENDOR_PREFIX.length()));
332             } catch (NumberFormatException ex) {
333                 return ProgramSelector.IDENTIFIER_TYPE_INVALID;
334             }
335             if (idx > ProgramSelector.IDENTIFIER_TYPE_VENDOR_END
336                     - ProgramSelector.IDENTIFIER_TYPE_VENDOR_START) {
337                 return ProgramSelector.IDENTIFIER_TYPE_INVALID;
338             }
339             return ProgramSelector.IDENTIFIER_TYPE_VENDOR_START + idx;
340         }
341         return URI_TO_ID.get(typeUri);
342     }
343 
valueToUri(@onNull Identifier id)344     private static @NonNull String valueToUri(@NonNull Identifier id) {
345         long val = id.getValue();
346         switch (id.getType()) {
347             case ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY:
348             case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY:
349             case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY:
350             case ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL:
351                 return Long.toString(val);
352             default:
353                 return URI_HEX_PREFIX + Long.toHexString(val);
354         }
355     }
356 
uriToValue(@ullable String valUri)357     private static @Nullable Long uriToValue(@Nullable String valUri) {
358         if (valUri == null) return null;
359         try {
360             if (valUri.startsWith(URI_HEX_PREFIX)) {
361                 return Long.parseLong(valUri.substring(URI_HEX_PREFIX.length()), 16);
362             } else {
363                 return Long.parseLong(valUri, 10);
364             }
365         } catch (NumberFormatException ex) {
366             return null;
367         }
368     }
369 
370     /**
371      * Serialize {@link ProgramSelector} to URI.
372      *
373      * @param sel selector to serialize
374      * @return serialized form of selector
375      */
toUri(@onNull ProgramSelector sel)376     public static @Nullable Uri toUri(@NonNull ProgramSelector sel) {
377         Identifier pri = sel.getPrimaryId();
378         String priType = typeToUri(pri.getType());
379         // unsupported primary identifier, might be from future HAL revision
380         if (priType == null) return null;
381 
382         Uri.Builder uri = new Uri.Builder()
383                 .scheme(URI_SCHEME_BROADCASTRADIO)
384                 .authority(URI_AUTHORITY_PROGRAM)
385                 .appendPath(priType)
386                 .appendPath(valueToUri(pri));
387 
388         for (Identifier sec : sel.getSecondaryIds()) {
389             String secType = typeToUri(sec.getType());
390             if (secType == null) continue;  // skip unsupported secondary identifier
391             uri.appendQueryParameter(secType, valueToUri(sec));
392         }
393         return uri.build();
394     }
395 
396     /**
397      * Parse serialized {@link ProgramSelector}.
398      *
399      * @param uri URI-zed form of ProgramSelector
400      * @return de-serialized object or null, if couldn't parse
401      */
fromUri(@ullable Uri uri)402     public static @Nullable ProgramSelector fromUri(@Nullable Uri uri) {
403         if (uri == null) return null;
404 
405         if (!URI_SCHEME_BROADCASTRADIO.equals(uri.getScheme())) return null;
406         if (!URI_AUTHORITY_PROGRAM.equals(uri.getAuthority())) {
407             Log.w(TAG, "Unknown URI authority part (might be a future, unsupported version): "
408                     + uri.getAuthority());
409             return null;
410         }
411 
412         BiFunction<String, String, Identifier> parseComponents = (typeStr, valueStr) -> {
413             int type = uriToType(typeStr);
414             Long value = uriToValue(valueStr);
415             if (type == ProgramSelector.IDENTIFIER_TYPE_INVALID || value == null) return null;
416             return new Identifier(type, value);
417         };
418 
419         List<String> priUri = uri.getPathSegments();
420         if (priUri.size() != 2) return null;
421         Identifier pri = parseComponents.apply(priUri.get(0), priUri.get(1));
422         if (pri == null) return null;
423 
424         String query = uri.getQuery();
425         List<Identifier> secIds = new ArrayList<>();
426         if (query != null) {
427             for (String secPair : query.split("&")) {
428                 String[] secStr = secPair.split("=");
429                 if (secStr.length != 2) continue;
430                 Identifier sec = parseComponents.apply(secStr[0], secStr[1]);
431                 if (sec != null) secIds.add(sec);
432             }
433         }
434 
435         return newProgramSelector(pri, secIds.toArray(new Identifier[secIds.size()]));
436     }
437 
438     /**
439      * Proposed extensions to android.hardware.radio.ProgramSelector.Identifier.
440      *
441      * They might eventually get pushed to the framework.
442      */
443     public static class IdentifierExt {
444         /**
445          * Decode {@link ProgramSelector#IDENTIFIER_TYPE_HD_STATION_ID_EXT} value.
446          *
447          * @param id identifier to decode
448          * @return value decoder
449          */
asHdPrimary(@onNull Identifier id)450         public static @Nullable HdPrimary asHdPrimary(@NonNull Identifier id) {
451             if (id.getType() == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT) {
452                 return new HdPrimary(id.getValue());
453             }
454             return null;
455         }
456 
457         /**
458          * Decoder of {@link ProgramSelector#IDENTIFIER_TYPE_HD_STATION_ID_EXT} value.
459          *
460          * When pushed to the framework, it will be non-static class referring
461          * to the original value.
462          */
463         public static class HdPrimary {
464             /* For mValue format (bit shifts and bit masks), please refer to
465              * HD_STATION_ID_EXT from broadcastradio HAL 2.0.
466              */
467             private final long mValue;
468 
HdPrimary(long value)469             private HdPrimary(long value) {
470                 mValue = value;
471             }
472 
getStationId()473             public long getStationId() {
474                 return mValue & 0xFFFFFFFF;
475             }
476 
getSubchannel()477             public int getSubchannel() {
478                 return (int) ((mValue >>> 32) & 0xF);
479             }
480 
getFrequency()481             public int getFrequency() {
482                 return (int) ((mValue >>> (32 + 4)) & 0x3FFFF);
483             }
484         }
485     }
486 }
487