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