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