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