diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
commit | 10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch) | |
tree | 8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/hardware/radio | |
parent | 677516fb6b6f207d373984757d3d9450474b6b00 (diff) | |
download | android-28-10d07c88d69cc64f73a069163e7ea5ba2519a099.tar.gz |
Import Android SDK Platform PI [4335822]
/google/data/ro/projects/android/fetch_artifact \
--bid 4335822 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4335822.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
Diffstat (limited to 'android/hardware/radio')
-rw-r--r-- | android/hardware/radio/ProgramSelector.java | 502 | ||||
-rw-r--r-- | android/hardware/radio/RadioManager.java | 1673 | ||||
-rw-r--r-- | android/hardware/radio/RadioMetadata.java | 578 | ||||
-rw-r--r-- | android/hardware/radio/RadioTuner.java | 435 | ||||
-rw-r--r-- | android/hardware/radio/TunerAdapter.java | 271 | ||||
-rw-r--r-- | android/hardware/radio/TunerCallbackAdapter.java | 97 |
6 files changed, 3556 insertions, 0 deletions
diff --git a/android/hardware/radio/ProgramSelector.java b/android/hardware/radio/ProgramSelector.java new file mode 100644 index 00000000..2211cee9 --- /dev/null +++ b/android/hardware/radio/ProgramSelector.java @@ -0,0 +1,502 @@ +/** + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.radio; + +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +/** + * A set of identifiers necessary to tune to a given station. + * + * This can hold various identifiers, like + * - AM/FM frequency + * - HD Radio subchannel + * - DAB channel info + * + * The primary ID uniquely identifies a station and can be used for equality + * check. The secondary IDs are supplementary and can speed up tuning process, + * but the primary ID is sufficient (ie. after a full band scan). + * + * Two selectors with different secondary IDs, but the same primary ID are + * considered equal. In particular, secondary IDs vector may get updated for + * an entry on the program list (ie. when a better frequency for a given + * station is found). + * + * The primaryId of a given programType MUST be of a specific type: + * - AM, FM: RDS_PI if the station broadcasts RDS, AMFM_FREQUENCY otherwise; + * - AM_HD, FM_HD: HD_STATION_ID_EXT; + * - DAB: DAB_SIDECC; + * - DRMO: DRMO_SERVICE_ID; + * - SXM: SXM_SERVICE_ID; + * - VENDOR: VENDOR_PRIMARY. + * @hide + */ +@SystemApi +public final class ProgramSelector implements Parcelable { + /** Analogue AM radio (with or without RDS). */ + public static final int PROGRAM_TYPE_AM = 1; + /** analogue FM radio (with or without RDS). */ + public static final int PROGRAM_TYPE_FM = 2; + /** AM HD Radio. */ + public static final int PROGRAM_TYPE_AM_HD = 3; + /** FM HD Radio. */ + public static final int PROGRAM_TYPE_FM_HD = 4; + /** Digital audio broadcasting. */ + public static final int PROGRAM_TYPE_DAB = 5; + /** Digital Radio Mondiale. */ + public static final int PROGRAM_TYPE_DRMO = 6; + /** SiriusXM Satellite Radio. */ + public static final int PROGRAM_TYPE_SXM = 7; + /** Vendor-specific, not synced across devices. */ + public static final int PROGRAM_TYPE_VENDOR_START = 1000; + public static final int PROGRAM_TYPE_VENDOR_END = 1999; + @IntDef(prefix = { "PROGRAM_TYPE_" }, value = { + PROGRAM_TYPE_AM, + PROGRAM_TYPE_FM, + PROGRAM_TYPE_AM_HD, + PROGRAM_TYPE_FM_HD, + PROGRAM_TYPE_DAB, + PROGRAM_TYPE_DRMO, + PROGRAM_TYPE_SXM, + }) + @IntRange(from = PROGRAM_TYPE_VENDOR_START, to = PROGRAM_TYPE_VENDOR_END) + @Retention(RetentionPolicy.SOURCE) + public @interface ProgramType {} + + /** kHz */ + public static final int IDENTIFIER_TYPE_AMFM_FREQUENCY = 1; + /** 16bit */ + public static final int IDENTIFIER_TYPE_RDS_PI = 2; + /** + * 64bit compound primary identifier for HD Radio. + * + * Consists of (from the LSB): + * - 32bit: Station ID number; + * - 4bit: HD_SUBCHANNEL; + * - 18bit: AMFM_FREQUENCY. + * The remaining bits should be set to zeros when writing on the chip side + * and ignored when read. + */ + public static final int IDENTIFIER_TYPE_HD_STATION_ID_EXT = 3; + /** + * HD Radio subchannel - a value of range 0-7. + * + * The subchannel index is 0-based (where 0 is MPS and 1..7 are SPS), + * as opposed to HD Radio standard (where it's 1-based). + */ + public static final int IDENTIFIER_TYPE_HD_SUBCHANNEL = 4; + /** + * 24bit compound primary identifier for DAB. + * + * Consists of (from the LSB): + * - 16bit: SId; + * - 8bit: ECC code. + * The remaining bits should be set to zeros when writing on the chip side + * and ignored when read. + */ + public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5; + /** 16bit */ + public static final int IDENTIFIER_TYPE_DAB_ENSEMBLE = 6; + /** 12bit */ + public static final int IDENTIFIER_TYPE_DAB_SCID = 7; + /** kHz */ + public static final int IDENTIFIER_TYPE_DAB_FREQUENCY = 8; + /** 24bit */ + public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9; + /** kHz */ + public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10; + /** 1: AM, 2:FM */ + public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11; + /** 32bit */ + public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12; + /** 0-999 range */ + public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13; + /** + * Primary identifier for vendor-specific radio technology. + * The value format is determined by a vendor. + * + * It must not be used in any other programType than corresponding VENDOR + * type between VENDOR_START and VENDOR_END (eg. identifier type 1015 must + * not be used in any program type other than 1015). + */ + public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_START = PROGRAM_TYPE_VENDOR_START; + public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_END = PROGRAM_TYPE_VENDOR_END; + @IntDef(prefix = { "IDENTIFIER_TYPE_" }, value = { + IDENTIFIER_TYPE_AMFM_FREQUENCY, + IDENTIFIER_TYPE_RDS_PI, + IDENTIFIER_TYPE_HD_STATION_ID_EXT, + IDENTIFIER_TYPE_HD_SUBCHANNEL, + IDENTIFIER_TYPE_DAB_SIDECC, + IDENTIFIER_TYPE_DAB_ENSEMBLE, + IDENTIFIER_TYPE_DAB_SCID, + IDENTIFIER_TYPE_DAB_FREQUENCY, + IDENTIFIER_TYPE_DRMO_SERVICE_ID, + IDENTIFIER_TYPE_DRMO_FREQUENCY, + IDENTIFIER_TYPE_DRMO_MODULATION, + IDENTIFIER_TYPE_SXM_SERVICE_ID, + IDENTIFIER_TYPE_SXM_CHANNEL, + }) + @IntRange(from = IDENTIFIER_TYPE_VENDOR_PRIMARY_START, to = IDENTIFIER_TYPE_VENDOR_PRIMARY_END) + @Retention(RetentionPolicy.SOURCE) + public @interface IdentifierType {} + + private final @ProgramType int mProgramType; + private final @NonNull Identifier mPrimaryId; + private final @NonNull Identifier[] mSecondaryIds; + private final @NonNull long[] mVendorIds; + + /** + * Constructor for ProgramSelector. + * + * It's not desired to modify selector objects, so all its fields are initialized at creation. + * + * Identifier lists must not contain any nulls, but can itself be null to be interpreted + * as empty list at object creation. + * + * @param programType type of a radio technology. + * @param primaryId primary program identifier. + * @param secondaryIds list of secondary program identifiers. + * @param vendorIds list of vendor-specific program identifiers. + */ + public ProgramSelector(@ProgramType int programType, @NonNull Identifier primaryId, + @Nullable Identifier[] secondaryIds, @Nullable long[] vendorIds) { + if (secondaryIds == null) secondaryIds = new Identifier[0]; + if (vendorIds == null) vendorIds = new long[0]; + if (Stream.of(secondaryIds).anyMatch(id -> id == null)) { + throw new IllegalArgumentException("secondaryIds list must not contain nulls"); + } + mProgramType = programType; + mPrimaryId = Objects.requireNonNull(primaryId); + mSecondaryIds = secondaryIds; + mVendorIds = vendorIds; + } + + /** + * Type of a radio technology. + * + * @return program type. + */ + public @ProgramType int getProgramType() { + return mProgramType; + } + + /** + * Primary program identifier uniquely identifies a station and is used to + * determine equality between two ProgramSelectors. + * + * @return primary identifier. + */ + public @NonNull Identifier getPrimaryId() { + return mPrimaryId; + } + + /** + * Secondary program identifier is not required for tuning, but may make it + * faster or more reliable. + * + * @return secondary identifier list, must not be modified. + */ + public @NonNull Identifier[] getSecondaryIds() { + return mSecondaryIds; + } + + /** + * Looks up an identifier of a given type (either primary or secondary). + * + * If there are multiple identifiers if a given type, then first in order (where primary id is + * before any secondary) is selected. + * + * @param type type of identifier. + * @return identifier value, if found. + * @throws IllegalArgumentException, if not found. + */ + public long getFirstId(@IdentifierType int type) { + if (mPrimaryId.getType() == type) return mPrimaryId.getValue(); + for (Identifier id : mSecondaryIds) { + if (id.getType() == type) return id.getValue(); + } + throw new IllegalArgumentException("Identifier " + type + " not found"); + } + + /** + * Looks up all identifier of a given type (either primary or secondary). + * + * Some identifiers may be provided multiple times, for example + * IDENTIFIER_TYPE_AMFM_FREQUENCY for FM Alternate Frequencies. + * + * @param type type of identifier. + * @return a list of identifiers, generated on each call. May be modified. + */ + public @NonNull Identifier[] getAllIds(@IdentifierType int type) { + List<Identifier> out = new ArrayList<>(); + + if (mPrimaryId.getType() == type) out.add(mPrimaryId); + for (Identifier id : mSecondaryIds) { + if (id.getType() == type) out.add(id); + } + + return out.toArray(new Identifier[out.size()]); + } + + /** + * Vendor identifiers are passed as-is to the HAL implementation, + * preserving elements order. + * + * @return a array of vendor identifiers, must not be modified. + */ + public @NonNull long[] getVendorIds() { + return mVendorIds; + } + + /** + * Builds new ProgramSelector for AM/FM frequency. + * + * @param band the band. + * @param frequencyKhz the frequency in kHz. + * @return new ProgramSelector object representing given frequency. + * @throws IllegalArgumentException if provided frequency is out of bounds. + */ + public static @NonNull ProgramSelector createAmFmSelector( + @RadioManager.Band int band, int frequencyKhz) { + return createAmFmSelector(band, frequencyKhz, 0); + } + + /** + * Checks, if a given AM/FM frequency is roughly valid and in correct unit. + * + * It does not check the range precisely. In particular, it may be way off for certain regions. + * The main purpose is to avoid passing inproper units, ie. MHz instead of kHz. + * + * @param isAm true, if AM, false if FM. + * @param frequencyKhz the frequency in kHz. + * @return true, if the frequency is rougly valid. + */ + private static boolean isValidAmFmFrequency(boolean isAm, int frequencyKhz) { + if (isAm) { + return frequencyKhz > 150 && frequencyKhz < 30000; + } else { + return frequencyKhz > 60000 && frequencyKhz < 110000; + } + } + + /** + * Builds new ProgramSelector for AM/FM frequency. + * + * This method variant supports HD Radio subchannels, but it's undesirable to + * select them manually. Instead, the value should be retrieved from program list. + * + * @param band the band. + * @param frequencyKhz the frequency in kHz. + * @param subChannel 1-based HD Radio subchannel. + * @return new ProgramSelector object representing given frequency. + * @throws IllegalArgumentException if provided frequency is out of bounds, + * or tried setting a subchannel for analog AM/FM. + */ + public static @NonNull ProgramSelector createAmFmSelector( + @RadioManager.Band int band, int frequencyKhz, int subChannel) { + boolean isAm = (band == RadioManager.BAND_AM || band == RadioManager.BAND_AM_HD); + boolean isDigital = (band == RadioManager.BAND_AM_HD || band == RadioManager.BAND_FM_HD); + if (!isAm && !isDigital && band != RadioManager.BAND_FM) { + throw new IllegalArgumentException("Unknown band: " + band); + } + if (subChannel < 0 || subChannel > 8) { + throw new IllegalArgumentException("Invalid subchannel: " + subChannel); + } + if (subChannel > 0 && !isDigital) { + throw new IllegalArgumentException("Subchannels are not supported for non-HD radio"); + } + if (!isValidAmFmFrequency(isAm, frequencyKhz)) { + throw new IllegalArgumentException("Provided value is not a valid AM/FM frequency"); + } + + // We can't use AM_HD or FM_HD, because we don't know HD station ID. + @ProgramType int programType = isAm ? PROGRAM_TYPE_AM : PROGRAM_TYPE_FM; + Identifier primary = new Identifier(IDENTIFIER_TYPE_AMFM_FREQUENCY, frequencyKhz); + + Identifier[] secondary = null; + if (subChannel > 0) { + /* Stating sub channel for non-HD AM/FM does not give any guarantees, + * but we can't do much more without HD station ID. + * + * The legacy APIs had 1-based subChannels, while ProgramSelector is 0-based. + */ + secondary = new Identifier[]{ + new Identifier(IDENTIFIER_TYPE_HD_SUBCHANNEL, subChannel - 1)}; + } + + return new ProgramSelector(programType, primary, secondary, null); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("ProgramSelector(type=").append(mProgramType) + .append(", primary=").append(mPrimaryId); + if (mSecondaryIds.length > 0) sb.append(", secondary=").append(mSecondaryIds); + if (mVendorIds.length > 0) sb.append(", vendor=").append(mVendorIds); + sb.append(")"); + return sb.toString(); + } + + @Override + public int hashCode() { + // secondaryIds and vendorIds are ignored for equality/hashing + return Objects.hash(mProgramType, mPrimaryId); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof ProgramSelector)) return false; + ProgramSelector other = (ProgramSelector) obj; + // secondaryIds and vendorIds are ignored for equality/hashing + return other.getProgramType() == mProgramType && mPrimaryId.equals(other.getPrimaryId()); + } + + private ProgramSelector(Parcel in) { + mProgramType = in.readInt(); + mPrimaryId = in.readTypedObject(Identifier.CREATOR); + mSecondaryIds = in.createTypedArray(Identifier.CREATOR); + if (Stream.of(mSecondaryIds).anyMatch(id -> id == null)) { + throw new IllegalArgumentException("secondaryIds list must not contain nulls"); + } + mVendorIds = in.createLongArray(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mProgramType); + dest.writeTypedObject(mPrimaryId, 0); + dest.writeTypedArray(mSecondaryIds, 0); + dest.writeLongArray(mVendorIds); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<ProgramSelector> CREATOR = + new Parcelable.Creator<ProgramSelector>() { + public ProgramSelector createFromParcel(Parcel in) { + return new ProgramSelector(in); + } + + public ProgramSelector[] newArray(int size) { + return new ProgramSelector[size]; + } + }; + + /** + * A single program identifier component, eg. frequency or channel ID. + * + * The long value field holds the value in format described in comments for + * IdentifierType constants. + */ + public static final class Identifier implements Parcelable { + private final @IdentifierType int mType; + private final long mValue; + + public Identifier(@IdentifierType int type, long value) { + mType = type; + mValue = value; + } + + /** + * Type of an identifier. + * + * @return type of an identifier. + */ + public @IdentifierType int getType() { + return mType; + } + + /** + * Value of an identifier. + * + * Its meaning depends on identifier type, ie. for IDENTIFIER_TYPE_AMFM_FREQUENCY type, + * the value is a frequency in kHz. + * + * The range of a value depends on its type; it does not always require the whole long + * range. Casting to necessary type (ie. int) without range checking is correct in front-end + * code - any range violations are either errors in the framework or in the + * HAL implementation. For example, IDENTIFIER_TYPE_AMFM_FREQUENCY always fits in int, + * as Integer.MAX_VALUE would mean 2.1THz. + * + * @return value of an identifier. + */ + public long getValue() { + return mValue; + } + + @Override + public String toString() { + return "Identifier(" + mType + ", " + mValue + ")"; + } + + @Override + public int hashCode() { + return Objects.hash(mType, mValue); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Identifier)) return false; + Identifier other = (Identifier) obj; + return other.getType() == mType && other.getValue() == mValue; + } + + private Identifier(Parcel in) { + mType = in.readInt(); + mValue = in.readLong(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mType); + dest.writeLong(mValue); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<Identifier> CREATOR = + new Parcelable.Creator<Identifier>() { + public Identifier createFromParcel(Parcel in) { + return new Identifier(in); + } + + public Identifier[] newArray(int size) { + return new Identifier[size]; + } + }; + } +} diff --git a/android/hardware/radio/RadioManager.java b/android/hardware/radio/RadioManager.java new file mode 100644 index 00000000..4f4361f6 --- /dev/null +++ b/android/hardware/radio/RadioManager.java @@ -0,0 +1,1673 @@ +/** + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.radio; + +import android.Manifest; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.annotation.SystemService; +import android.content.Context; +import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.ServiceManager.ServiceNotFoundException; +import android.text.TextUtils; +import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * The RadioManager class allows to control a broadcast radio tuner present on the device. + * It provides data structures and methods to query for available radio modules, list their + * properties and open an interface to control tuning operations and receive callbacks when + * asynchronous operations complete or events occur. + * @hide + */ +@SystemApi +@SystemService(Context.RADIO_SERVICE) +public class RadioManager { + private static final String TAG = "BroadcastRadio.manager"; + + /** Method return status: successful operation */ + public static final int STATUS_OK = 0; + /** Method return status: unspecified error */ + public static final int STATUS_ERROR = Integer.MIN_VALUE; + /** Method return status: permission denied */ + public static final int STATUS_PERMISSION_DENIED = -1; + /** Method return status: initialization failure */ + public static final int STATUS_NO_INIT = -19; + /** Method return status: invalid argument provided */ + public static final int STATUS_BAD_VALUE = -22; + /** Method return status: cannot reach service */ + public static final int STATUS_DEAD_OBJECT = -32; + /** Method return status: invalid or out of sequence operation */ + public static final int STATUS_INVALID_OPERATION = -38; + /** Method return status: time out before operation completion */ + public static final int STATUS_TIMED_OUT = -110; + + + // keep in sync with radio_class_t in /system/core/incluse/system/radio.h + /** Radio module class supporting FM (including HD radio) and AM */ + public static final int CLASS_AM_FM = 0; + /** Radio module class supporting satellite radio */ + public static final int CLASS_SAT = 1; + /** Radio module class supporting Digital terrestrial radio */ + public static final int CLASS_DT = 2; + + public static final int BAND_INVALID = -1; + /** AM radio band (LW/MW/SW). + * @see BandDescriptor */ + public static final int BAND_AM = 0; + /** FM radio band. + * @see BandDescriptor */ + public static final int BAND_FM = 1; + /** FM HD radio or DRM band. + * @see BandDescriptor */ + public static final int BAND_FM_HD = 2; + /** AM HD radio or DRM band. + * @see BandDescriptor */ + public static final int BAND_AM_HD = 3; + @IntDef(prefix = { "BAND_" }, value = { + BAND_INVALID, + BAND_AM, + BAND_FM, + BAND_AM_HD, + BAND_FM_HD, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Band {} + + // keep in sync with radio_region_t in /system/core/incluse/system/radio.h + /** Africa, Europe. + * @see BandDescriptor */ + public static final int REGION_ITU_1 = 0; + /** Americas. + * @see BandDescriptor */ + public static final int REGION_ITU_2 = 1; + /** Russia. + * @see BandDescriptor */ + public static final int REGION_OIRT = 2; + /** Japan. + * @see BandDescriptor */ + public static final int REGION_JAPAN = 3; + /** Korea. + * @see BandDescriptor */ + public static final int REGION_KOREA = 4; + + private static void writeStringMap(@NonNull Parcel dest, @NonNull Map<String, String> map) { + dest.writeInt(map.size()); + for (Map.Entry<String, String> entry : map.entrySet()) { + dest.writeString(entry.getKey()); + dest.writeString(entry.getValue()); + } + } + + private static @NonNull Map<String, String> readStringMap(@NonNull Parcel in) { + int size = in.readInt(); + Map<String, String> map = new HashMap<>(); + while (size-- > 0) { + String key = in.readString(); + String value = in.readString(); + map.put(key, value); + } + return map; + } + + /***************************************************************************** + * Lists properties, options and radio bands supported by a given broadcast radio module. + * Each module has a unique ID used to address it when calling RadioManager APIs. + * Module properties are returned by {@link #listModules(List <ModuleProperties>)} method. + ****************************************************************************/ + public static class ModuleProperties implements Parcelable { + + private final int mId; + @NonNull private final String mServiceName; + private final int mClassId; + private final String mImplementor; + private final String mProduct; + private final String mVersion; + private final String mSerial; + private final int mNumTuners; + private final int mNumAudioSources; + private final boolean mIsCaptureSupported; + private final BandDescriptor[] mBands; + private final boolean mIsBgScanSupported; + private final Set<Integer> mSupportedProgramTypes; + private final Set<Integer> mSupportedIdentifierTypes; + @NonNull private final Map<String, String> mVendorInfo; + + ModuleProperties(int id, String serviceName, int classId, String implementor, + String product, String version, String serial, int numTuners, int numAudioSources, + boolean isCaptureSupported, BandDescriptor[] bands, boolean isBgScanSupported, + @ProgramSelector.ProgramType int[] supportedProgramTypes, + @ProgramSelector.IdentifierType int[] supportedIdentifierTypes, + Map<String, String> vendorInfo) { + mId = id; + mServiceName = TextUtils.isEmpty(serviceName) ? "default" : serviceName; + mClassId = classId; + mImplementor = implementor; + mProduct = product; + mVersion = version; + mSerial = serial; + mNumTuners = numTuners; + mNumAudioSources = numAudioSources; + mIsCaptureSupported = isCaptureSupported; + mBands = bands; + mIsBgScanSupported = isBgScanSupported; + mSupportedProgramTypes = arrayToSet(supportedProgramTypes); + mSupportedIdentifierTypes = arrayToSet(supportedIdentifierTypes); + mVendorInfo = (vendorInfo == null) ? new HashMap<>() : vendorInfo; + } + + private static Set<Integer> arrayToSet(int[] arr) { + return Arrays.stream(arr).boxed().collect(Collectors.toSet()); + } + + private static int[] setToArray(Set<Integer> set) { + return set.stream().mapToInt(Integer::intValue).toArray(); + } + + /** Unique module identifier provided by the native service. + * For use with {@link #openTuner(int, BandConfig, boolean, Callback, Handler)}. + * @return the radio module unique identifier. + */ + public int getId() { + return mId; + } + + /** + * Module service (driver) name as registered with HIDL. + * @return the module service name. + */ + public @NonNull String getServiceName() { + return mServiceName; + } + + /** Module class identifier: {@link #CLASS_AM_FM}, {@link #CLASS_SAT}, {@link #CLASS_DT} + * @return the radio module class identifier. + */ + public int getClassId() { + return mClassId; + } + + /** Human readable broadcast radio module implementor + * @return the name of the radio module implementator. + */ + public String getImplementor() { + return mImplementor; + } + + /** Human readable broadcast radio module product name + * @return the radio module product name. + */ + public String getProduct() { + return mProduct; + } + + /** Human readable broadcast radio module version number + * @return the radio module version. + */ + public String getVersion() { + return mVersion; + } + + /** Radio module serial number. + * Can be used for subscription services. + * @return the radio module serial number. + */ + public String getSerial() { + return mSerial; + } + + /** Number of tuners available. + * This is the number of tuners that can be open simultaneously. + * @return the number of tuners supported. + */ + public int getNumTuners() { + return mNumTuners; + } + + /** Number tuner audio sources available. Must be less or equal to getNumTuners(). + * When more than one tuner is supported, one is usually for playback and has one + * associated audio source and the other is for pre scanning and building a + * program list. + * @return the number of audio sources available. + */ + public int getNumAudioSources() { + return mNumAudioSources; + } + + /** {@code true} if audio capture is possible from radio tuner output. + * This indicates if routing to audio devices not connected to the same HAL as the FM radio + * is possible (e.g. to USB) or DAR (Digital Audio Recorder) feature can be implemented. + * @return {@code true} if audio capture is possible, {@code false} otherwise. + */ + public boolean isCaptureSupported() { + return mIsCaptureSupported; + } + + /** + * {@code true} if the module supports background scanning. At the given time it may not + * be available though, see {@link RadioTuner#startBackgroundScan()}. + * + * @return {@code true} if background scanning is supported (not necessary available + * at a given time), {@code false} otherwise. + */ + public boolean isBackgroundScanningSupported() { + return mIsBgScanSupported; + } + + /** + * Checks, if a given program type is supported by this tuner. + * + * If a program type is supported by radio module, it means it can tune + * to ProgramSelector of a given type. + * + * @return {@code true} if a given program type is supported. + */ + public boolean isProgramTypeSupported(@ProgramSelector.ProgramType int type) { + return mSupportedProgramTypes.contains(type); + } + + /** + * Checks, if a given program identifier is supported by this tuner. + * + * If an identifier is supported by radio module, it means it can use it for + * tuning to ProgramSelector with either primary or secondary Identifier of + * a given type. + * + * @return {@code true} if a given program type is supported. + */ + public boolean isProgramIdentifierSupported(@ProgramSelector.IdentifierType int type) { + return mSupportedIdentifierTypes.contains(type); + } + + /** + * A map of vendor-specific opaque strings, passed from HAL without changes. + * Format of these strings can vary across vendors. + * + * It may be used for extra features, that's not supported by a platform, + * for example: preset-slots=6; ultra-hd-capable=false. + * + * Keys must be prefixed with unique vendor Java-style namespace, + * eg. 'com.somecompany.parameter1'. + */ + public @NonNull Map<String, String> getVendorInfo() { + return mVendorInfo; + } + + /** List of descriptors for all bands supported by this module. + * @return an array of {@link BandDescriptor}. + */ + public BandDescriptor[] getBands() { + return mBands; + } + + private ModuleProperties(Parcel in) { + mId = in.readInt(); + String serviceName = in.readString(); + mServiceName = TextUtils.isEmpty(serviceName) ? "default" : serviceName; + mClassId = in.readInt(); + mImplementor = in.readString(); + mProduct = in.readString(); + mVersion = in.readString(); + mSerial = in.readString(); + mNumTuners = in.readInt(); + mNumAudioSources = in.readInt(); + mIsCaptureSupported = in.readInt() == 1; + Parcelable[] tmp = in.readParcelableArray(BandDescriptor.class.getClassLoader()); + mBands = new BandDescriptor[tmp.length]; + for (int i = 0; i < tmp.length; i++) { + mBands[i] = (BandDescriptor) tmp[i]; + } + mIsBgScanSupported = in.readInt() == 1; + mSupportedProgramTypes = arrayToSet(in.createIntArray()); + mSupportedIdentifierTypes = arrayToSet(in.createIntArray()); + mVendorInfo = readStringMap(in); + } + + public static final Parcelable.Creator<ModuleProperties> CREATOR + = new Parcelable.Creator<ModuleProperties>() { + public ModuleProperties createFromParcel(Parcel in) { + return new ModuleProperties(in); + } + + public ModuleProperties[] newArray(int size) { + return new ModuleProperties[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mId); + dest.writeString(mServiceName); + dest.writeInt(mClassId); + dest.writeString(mImplementor); + dest.writeString(mProduct); + dest.writeString(mVersion); + dest.writeString(mSerial); + dest.writeInt(mNumTuners); + dest.writeInt(mNumAudioSources); + dest.writeInt(mIsCaptureSupported ? 1 : 0); + dest.writeParcelableArray(mBands, flags); + dest.writeInt(mIsBgScanSupported ? 1 : 0); + dest.writeIntArray(setToArray(mSupportedProgramTypes)); + dest.writeIntArray(setToArray(mSupportedIdentifierTypes)); + writeStringMap(dest, mVendorInfo); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "ModuleProperties [mId=" + mId + + ", mServiceName=" + mServiceName + ", mClassId=" + mClassId + + ", mImplementor=" + mImplementor + ", mProduct=" + mProduct + + ", mVersion=" + mVersion + ", mSerial=" + mSerial + + ", mNumTuners=" + mNumTuners + + ", mNumAudioSources=" + mNumAudioSources + + ", mIsCaptureSupported=" + mIsCaptureSupported + + ", mIsBgScanSupported=" + mIsBgScanSupported + + ", mBands=" + Arrays.toString(mBands) + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mId; + result = prime * result + mServiceName.hashCode(); + result = prime * result + mClassId; + result = prime * result + ((mImplementor == null) ? 0 : mImplementor.hashCode()); + result = prime * result + ((mProduct == null) ? 0 : mProduct.hashCode()); + result = prime * result + ((mVersion == null) ? 0 : mVersion.hashCode()); + result = prime * result + ((mSerial == null) ? 0 : mSerial.hashCode()); + result = prime * result + mNumTuners; + result = prime * result + mNumAudioSources; + result = prime * result + (mIsCaptureSupported ? 1 : 0); + result = prime * result + Arrays.hashCode(mBands); + result = prime * result + (mIsBgScanSupported ? 1 : 0); + result = prime * result + mVendorInfo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof ModuleProperties)) + return false; + ModuleProperties other = (ModuleProperties) obj; + if (mId != other.getId()) + return false; + if (!TextUtils.equals(mServiceName, other.mServiceName)) return false; + if (mClassId != other.getClassId()) + return false; + if (mImplementor == null) { + if (other.getImplementor() != null) + return false; + } else if (!mImplementor.equals(other.getImplementor())) + return false; + if (mProduct == null) { + if (other.getProduct() != null) + return false; + } else if (!mProduct.equals(other.getProduct())) + return false; + if (mVersion == null) { + if (other.getVersion() != null) + return false; + } else if (!mVersion.equals(other.getVersion())) + return false; + if (mSerial == null) { + if (other.getSerial() != null) + return false; + } else if (!mSerial.equals(other.getSerial())) + return false; + if (mNumTuners != other.getNumTuners()) + return false; + if (mNumAudioSources != other.getNumAudioSources()) + return false; + if (mIsCaptureSupported != other.isCaptureSupported()) + return false; + if (!Arrays.equals(mBands, other.getBands())) + return false; + if (mIsBgScanSupported != other.isBackgroundScanningSupported()) + return false; + if (!mVendorInfo.equals(other.mVendorInfo)) return false; + return true; + } + } + + /** Radio band descriptor: an element in ModuleProperties bands array. + * It is either an instance of {@link FmBandDescriptor} or {@link AmBandDescriptor} */ + public static class BandDescriptor implements Parcelable { + + private final int mRegion; + private final int mType; + private final int mLowerLimit; + private final int mUpperLimit; + private final int mSpacing; + + BandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing) { + if (type != BAND_AM && type != BAND_FM && type != BAND_FM_HD && type != BAND_AM_HD) { + throw new IllegalArgumentException("Unsupported band: " + type); + } + mRegion = region; + mType = type; + mLowerLimit = lowerLimit; + mUpperLimit = upperLimit; + mSpacing = spacing; + } + + /** Region this band applies to. E.g. {@link #REGION_ITU_1} + * @return the region this band is associated to. + */ + public int getRegion() { + return mRegion; + } + /** Band type, e.g {@link #BAND_FM}. Defines the subclass this descriptor can be cast to: + * <ul> + * <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}, </li> + * <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}, </li> + * </ul> + * @return the band type. + */ + public int getType() { + return mType; + } + + /** + * Checks if the band is either AM or AM_HD. + * + * @return {@code true}, if band is AM or AM_HD. + */ + public boolean isAmBand() { + return mType == BAND_AM || mType == BAND_AM_HD; + } + + /** + * Checks if the band is either FM or FM_HD. + * + * @return {@code true}, if band is FM or FM_HD. + */ + public boolean isFmBand() { + return mType == BAND_FM || mType == BAND_FM_HD; + } + + /** Lower band limit expressed in units according to band type. + * Currently all defined band types express channels as frequency in kHz + * @return the lower band limit. + */ + public int getLowerLimit() { + return mLowerLimit; + } + /** Upper band limit expressed in units according to band type. + * Currently all defined band types express channels as frequency in kHz + * @return the upper band limit. + */ + public int getUpperLimit() { + return mUpperLimit; + } + /** Channel spacing in units according to band type. + * Currently all defined band types express channels as frequency in kHz + * @return the channel spacing. + */ + public int getSpacing() { + return mSpacing; + } + + private BandDescriptor(Parcel in) { + mRegion = in.readInt(); + mType = in.readInt(); + mLowerLimit = in.readInt(); + mUpperLimit = in.readInt(); + mSpacing = in.readInt(); + } + + private static int lookupTypeFromParcel(Parcel in) { + int pos = in.dataPosition(); + in.readInt(); // skip region + int type = in.readInt(); + in.setDataPosition(pos); + return type; + } + + public static final Parcelable.Creator<BandDescriptor> CREATOR + = new Parcelable.Creator<BandDescriptor>() { + public BandDescriptor createFromParcel(Parcel in) { + int type = lookupTypeFromParcel(in); + switch (type) { + case BAND_FM: + case BAND_FM_HD: + return new FmBandDescriptor(in); + case BAND_AM: + case BAND_AM_HD: + return new AmBandDescriptor(in); + default: + throw new IllegalArgumentException("Unsupported band: " + type); + } + } + + public BandDescriptor[] newArray(int size) { + return new BandDescriptor[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRegion); + dest.writeInt(mType); + dest.writeInt(mLowerLimit); + dest.writeInt(mUpperLimit); + dest.writeInt(mSpacing); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "BandDescriptor [mRegion=" + mRegion + ", mType=" + mType + ", mLowerLimit=" + + mLowerLimit + ", mUpperLimit=" + mUpperLimit + ", mSpacing=" + mSpacing + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mRegion; + result = prime * result + mType; + result = prime * result + mLowerLimit; + result = prime * result + mUpperLimit; + result = prime * result + mSpacing; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof BandDescriptor)) + return false; + BandDescriptor other = (BandDescriptor) obj; + if (mRegion != other.getRegion()) + return false; + if (mType != other.getType()) + return false; + if (mLowerLimit != other.getLowerLimit()) + return false; + if (mUpperLimit != other.getUpperLimit()) + return false; + if (mSpacing != other.getSpacing()) + return false; + return true; + } + } + + /** FM band descriptor + * @see #BAND_FM + * @see #BAND_FM_HD */ + public static class FmBandDescriptor extends BandDescriptor { + private final boolean mStereo; + private final boolean mRds; + private final boolean mTa; + private final boolean mAf; + private final boolean mEa; + + FmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing, + boolean stereo, boolean rds, boolean ta, boolean af, boolean ea) { + super(region, type, lowerLimit, upperLimit, spacing); + mStereo = stereo; + mRds = rds; + mTa = ta; + mAf = af; + mEa = ea; + } + + /** Stereo is supported + * @return {@code true} if stereo is supported, {@code false} otherwise. + */ + public boolean isStereoSupported() { + return mStereo; + } + /** RDS or RBDS(if region is ITU2) is supported + * @return {@code true} if RDS or RBDS is supported, {@code false} otherwise. + */ + public boolean isRdsSupported() { + return mRds; + } + /** Traffic announcement is supported + * @return {@code true} if TA is supported, {@code false} otherwise. + */ + public boolean isTaSupported() { + return mTa; + } + /** Alternate Frequency Switching is supported + * @return {@code true} if AF switching is supported, {@code false} otherwise. + */ + public boolean isAfSupported() { + return mAf; + } + + /** Emergency Announcement is supported + * @return {@code true} if Emergency annoucement is supported, {@code false} otherwise. + */ + public boolean isEaSupported() { + return mEa; + } + + /* Parcelable implementation */ + private FmBandDescriptor(Parcel in) { + super(in); + mStereo = in.readByte() == 1; + mRds = in.readByte() == 1; + mTa = in.readByte() == 1; + mAf = in.readByte() == 1; + mEa = in.readByte() == 1; + } + + public static final Parcelable.Creator<FmBandDescriptor> CREATOR + = new Parcelable.Creator<FmBandDescriptor>() { + public FmBandDescriptor createFromParcel(Parcel in) { + return new FmBandDescriptor(in); + } + + public FmBandDescriptor[] newArray(int size) { + return new FmBandDescriptor[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeByte((byte) (mStereo ? 1 : 0)); + dest.writeByte((byte) (mRds ? 1 : 0)); + dest.writeByte((byte) (mTa ? 1 : 0)); + dest.writeByte((byte) (mAf ? 1 : 0)); + dest.writeByte((byte) (mEa ? 1 : 0)); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "FmBandDescriptor [ "+ super.toString() + " mStereo=" + mStereo + + ", mRds=" + mRds + ", mTa=" + mTa + ", mAf=" + mAf + + ", mEa =" + mEa + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + (mStereo ? 1 : 0); + result = prime * result + (mRds ? 1 : 0); + result = prime * result + (mTa ? 1 : 0); + result = prime * result + (mAf ? 1 : 0); + result = prime * result + (mEa ? 1 : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (!(obj instanceof FmBandDescriptor)) + return false; + FmBandDescriptor other = (FmBandDescriptor) obj; + if (mStereo != other.isStereoSupported()) + return false; + if (mRds != other.isRdsSupported()) + return false; + if (mTa != other.isTaSupported()) + return false; + if (mAf != other.isAfSupported()) + return false; + if (mEa != other.isEaSupported()) + return false; + return true; + } + } + + /** AM band descriptor. + * @see #BAND_AM */ + public static class AmBandDescriptor extends BandDescriptor { + + private final boolean mStereo; + + AmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing, + boolean stereo) { + super(region, type, lowerLimit, upperLimit, spacing); + mStereo = stereo; + } + + /** Stereo is supported + * @return {@code true} if stereo is supported, {@code false} otherwise. + */ + public boolean isStereoSupported() { + return mStereo; + } + + private AmBandDescriptor(Parcel in) { + super(in); + mStereo = in.readByte() == 1; + } + + public static final Parcelable.Creator<AmBandDescriptor> CREATOR + = new Parcelable.Creator<AmBandDescriptor>() { + public AmBandDescriptor createFromParcel(Parcel in) { + return new AmBandDescriptor(in); + } + + public AmBandDescriptor[] newArray(int size) { + return new AmBandDescriptor[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeByte((byte) (mStereo ? 1 : 0)); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "AmBandDescriptor [ "+ super.toString() + " mStereo=" + mStereo + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + (mStereo ? 1 : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (!(obj instanceof AmBandDescriptor)) + return false; + AmBandDescriptor other = (AmBandDescriptor) obj; + if (mStereo != other.isStereoSupported()) + return false; + return true; + } + } + + + /** Radio band configuration. */ + public static class BandConfig implements Parcelable { + + final BandDescriptor mDescriptor; + + BandConfig(BandDescriptor descriptor) { + mDescriptor = descriptor; + } + + BandConfig(int region, int type, int lowerLimit, int upperLimit, int spacing) { + mDescriptor = new BandDescriptor(region, type, lowerLimit, upperLimit, spacing); + } + + private BandConfig(Parcel in) { + mDescriptor = new BandDescriptor(in); + } + + BandDescriptor getDescriptor() { + return mDescriptor; + } + + /** Region this band applies to. E.g. {@link #REGION_ITU_1} + * @return the region associated with this band. + */ + public int getRegion() { + return mDescriptor.getRegion(); + } + /** Band type, e.g {@link #BAND_FM}. Defines the subclass this descriptor can be cast to: + * <ul> + * <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}, </li> + * <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}, </li> + * </ul> + * @return the band type. + */ + public int getType() { + return mDescriptor.getType(); + } + /** Lower band limit expressed in units according to band type. + * Currently all defined band types express channels as frequency in kHz + * @return the lower band limit. + */ + public int getLowerLimit() { + return mDescriptor.getLowerLimit(); + } + /** Upper band limit expressed in units according to band type. + * Currently all defined band types express channels as frequency in kHz + * @return the upper band limit. + */ + public int getUpperLimit() { + return mDescriptor.getUpperLimit(); + } + /** Channel spacing in units according to band type. + * Currently all defined band types express channels as frequency in kHz + * @return the channel spacing. + */ + public int getSpacing() { + return mDescriptor.getSpacing(); + } + + + public static final Parcelable.Creator<BandConfig> CREATOR + = new Parcelable.Creator<BandConfig>() { + public BandConfig createFromParcel(Parcel in) { + int type = BandDescriptor.lookupTypeFromParcel(in); + switch (type) { + case BAND_FM: + case BAND_FM_HD: + return new FmBandConfig(in); + case BAND_AM: + case BAND_AM_HD: + return new AmBandConfig(in); + default: + throw new IllegalArgumentException("Unsupported band: " + type); + } + } + + public BandConfig[] newArray(int size) { + return new BandConfig[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + mDescriptor.writeToParcel(dest, flags); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "BandConfig [ " + mDescriptor.toString() + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mDescriptor.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof BandConfig)) + return false; + BandConfig other = (BandConfig) obj; + BandDescriptor otherDesc = other.getDescriptor(); + if ((mDescriptor == null) != (otherDesc == null)) return false; + if (mDescriptor != null && !mDescriptor.equals(otherDesc)) return false; + return true; + } + } + + /** FM band configuration. + * @see #BAND_FM + * @see #BAND_FM_HD */ + public static class FmBandConfig extends BandConfig { + private final boolean mStereo; + private final boolean mRds; + private final boolean mTa; + private final boolean mAf; + private final boolean mEa; + + FmBandConfig(FmBandDescriptor descriptor) { + super((BandDescriptor)descriptor); + mStereo = descriptor.isStereoSupported(); + mRds = descriptor.isRdsSupported(); + mTa = descriptor.isTaSupported(); + mAf = descriptor.isAfSupported(); + mEa = descriptor.isEaSupported(); + } + + FmBandConfig(int region, int type, int lowerLimit, int upperLimit, int spacing, + boolean stereo, boolean rds, boolean ta, boolean af, boolean ea) { + super(region, type, lowerLimit, upperLimit, spacing); + mStereo = stereo; + mRds = rds; + mTa = ta; + mAf = af; + mEa = ea; + } + + /** Get stereo enable state + * @return the enable state. + */ + public boolean getStereo() { + return mStereo; + } + + /** Get RDS or RBDS(if region is ITU2) enable state + * @return the enable state. + */ + public boolean getRds() { + return mRds; + } + + /** Get Traffic announcement enable state + * @return the enable state. + */ + public boolean getTa() { + return mTa; + } + + /** Get Alternate Frequency Switching enable state + * @return the enable state. + */ + public boolean getAf() { + return mAf; + } + + /** + * Get Emergency announcement enable state + * @return the enable state. + */ + public boolean getEa() { + return mEa; + } + + private FmBandConfig(Parcel in) { + super(in); + mStereo = in.readByte() == 1; + mRds = in.readByte() == 1; + mTa = in.readByte() == 1; + mAf = in.readByte() == 1; + mEa = in.readByte() == 1; + } + + public static final Parcelable.Creator<FmBandConfig> CREATOR + = new Parcelable.Creator<FmBandConfig>() { + public FmBandConfig createFromParcel(Parcel in) { + return new FmBandConfig(in); + } + + public FmBandConfig[] newArray(int size) { + return new FmBandConfig[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeByte((byte) (mStereo ? 1 : 0)); + dest.writeByte((byte) (mRds ? 1 : 0)); + dest.writeByte((byte) (mTa ? 1 : 0)); + dest.writeByte((byte) (mAf ? 1 : 0)); + dest.writeByte((byte) (mEa ? 1 : 0)); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "FmBandConfig [" + super.toString() + + ", mStereo=" + mStereo + ", mRds=" + mRds + ", mTa=" + mTa + + ", mAf=" + mAf + ", mEa =" + mEa + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + (mStereo ? 1 : 0); + result = prime * result + (mRds ? 1 : 0); + result = prime * result + (mTa ? 1 : 0); + result = prime * result + (mAf ? 1 : 0); + result = prime * result + (mEa ? 1 : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (!(obj instanceof FmBandConfig)) + return false; + FmBandConfig other = (FmBandConfig) obj; + if (mStereo != other.mStereo) + return false; + if (mRds != other.mRds) + return false; + if (mTa != other.mTa) + return false; + if (mAf != other.mAf) + return false; + if (mEa != other.mEa) + return false; + return true; + } + + /** + * Builder class for {@link FmBandConfig} objects. + */ + public static class Builder { + private final BandDescriptor mDescriptor; + private boolean mStereo; + private boolean mRds; + private boolean mTa; + private boolean mAf; + private boolean mEa; + + /** + * Constructs a new Builder with the defaults from an {@link FmBandDescriptor} . + * @param descriptor the FmBandDescriptor defaults are read from . + */ + public Builder(FmBandDescriptor descriptor) { + mDescriptor = new BandDescriptor(descriptor.getRegion(), descriptor.getType(), + descriptor.getLowerLimit(), descriptor.getUpperLimit(), + descriptor.getSpacing()); + mStereo = descriptor.isStereoSupported(); + mRds = descriptor.isRdsSupported(); + mTa = descriptor.isTaSupported(); + mAf = descriptor.isAfSupported(); + mEa = descriptor.isEaSupported(); + } + + /** + * Constructs a new Builder from a given {@link FmBandConfig} + * @param config the FmBandConfig object whose data will be reused in the new Builder. + */ + public Builder(FmBandConfig config) { + mDescriptor = new BandDescriptor(config.getRegion(), config.getType(), + config.getLowerLimit(), config.getUpperLimit(), config.getSpacing()); + mStereo = config.getStereo(); + mRds = config.getRds(); + mTa = config.getTa(); + mAf = config.getAf(); + mEa = config.getEa(); + } + + /** + * Combines all of the parameters that have been set and return a new + * {@link FmBandConfig} object. + * @return a new {@link FmBandConfig} object + */ + public FmBandConfig build() { + FmBandConfig config = new FmBandConfig(mDescriptor.getRegion(), + mDescriptor.getType(), mDescriptor.getLowerLimit(), + mDescriptor.getUpperLimit(), mDescriptor.getSpacing(), + mStereo, mRds, mTa, mAf, mEa); + return config; + } + + /** Set stereo enable state + * @param state The new enable state. + * @return the same Builder instance. + */ + public Builder setStereo(boolean state) { + mStereo = state; + return this; + } + + /** Set RDS or RBDS(if region is ITU2) enable state + * @param state The new enable state. + * @return the same Builder instance. + */ + public Builder setRds(boolean state) { + mRds = state; + return this; + } + + /** Set Traffic announcement enable state + * @param state The new enable state. + * @return the same Builder instance. + */ + public Builder setTa(boolean state) { + mTa = state; + return this; + } + + /** Set Alternate Frequency Switching enable state + * @param state The new enable state. + * @return the same Builder instance. + */ + public Builder setAf(boolean state) { + mAf = state; + return this; + } + + /** Set Emergency Announcement enable state + * @param state The new enable state. + * @return the same Builder instance. + */ + public Builder setEa(boolean state) { + mEa = state; + return this; + } + }; + } + + /** AM band configuration. + * @see #BAND_AM */ + public static class AmBandConfig extends BandConfig { + private final boolean mStereo; + + AmBandConfig(AmBandDescriptor descriptor) { + super((BandDescriptor)descriptor); + mStereo = descriptor.isStereoSupported(); + } + + AmBandConfig(int region, int type, int lowerLimit, int upperLimit, int spacing, + boolean stereo) { + super(region, type, lowerLimit, upperLimit, spacing); + mStereo = stereo; + } + + /** Get stereo enable state + * @return the enable state. + */ + public boolean getStereo() { + return mStereo; + } + + private AmBandConfig(Parcel in) { + super(in); + mStereo = in.readByte() == 1; + } + + public static final Parcelable.Creator<AmBandConfig> CREATOR + = new Parcelable.Creator<AmBandConfig>() { + public AmBandConfig createFromParcel(Parcel in) { + return new AmBandConfig(in); + } + + public AmBandConfig[] newArray(int size) { + return new AmBandConfig[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeByte((byte) (mStereo ? 1 : 0)); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "AmBandConfig [" + super.toString() + + ", mStereo=" + mStereo + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + (mStereo ? 1 : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (!(obj instanceof AmBandConfig)) + return false; + AmBandConfig other = (AmBandConfig) obj; + if (mStereo != other.getStereo()) + return false; + return true; + } + + /** + * Builder class for {@link AmBandConfig} objects. + */ + public static class Builder { + private final BandDescriptor mDescriptor; + private boolean mStereo; + + /** + * Constructs a new Builder with the defaults from an {@link AmBandDescriptor} . + * @param descriptor the FmBandDescriptor defaults are read from . + */ + public Builder(AmBandDescriptor descriptor) { + mDescriptor = new BandDescriptor(descriptor.getRegion(), descriptor.getType(), + descriptor.getLowerLimit(), descriptor.getUpperLimit(), + descriptor.getSpacing()); + mStereo = descriptor.isStereoSupported(); + } + + /** + * Constructs a new Builder from a given {@link AmBandConfig} + * @param config the FmBandConfig object whose data will be reused in the new Builder. + */ + public Builder(AmBandConfig config) { + mDescriptor = new BandDescriptor(config.getRegion(), config.getType(), + config.getLowerLimit(), config.getUpperLimit(), config.getSpacing()); + mStereo = config.getStereo(); + } + + /** + * Combines all of the parameters that have been set and return a new + * {@link AmBandConfig} object. + * @return a new {@link AmBandConfig} object + */ + public AmBandConfig build() { + AmBandConfig config = new AmBandConfig(mDescriptor.getRegion(), + mDescriptor.getType(), mDescriptor.getLowerLimit(), + mDescriptor.getUpperLimit(), mDescriptor.getSpacing(), + mStereo); + return config; + } + + /** Set stereo enable state + * @param state The new enable state. + * @return the same Builder instance. + */ + public Builder setStereo(boolean state) { + mStereo = state; + return this; + } + }; + } + + /** Radio program information returned by + * {@link RadioTuner#getProgramInformation(RadioManager.ProgramInfo[])} */ + public static class ProgramInfo implements Parcelable { + + // sourced from hardware/interfaces/broadcastradio/1.1/types.hal + private static final int FLAG_LIVE = 1 << 0; + private static final int FLAG_MUTED = 1 << 1; + private static final int FLAG_TRAFFIC_PROGRAM = 1 << 2; + private static final int FLAG_TRAFFIC_ANNOUNCEMENT = 1 << 3; + + @NonNull private final ProgramSelector mSelector; + private final boolean mTuned; + private final boolean mStereo; + private final boolean mDigital; + private final int mFlags; + private final int mSignalStrength; + private final RadioMetadata mMetadata; + @NonNull private final Map<String, String> mVendorInfo; + + ProgramInfo(@NonNull ProgramSelector selector, boolean tuned, boolean stereo, + boolean digital, int signalStrength, RadioMetadata metadata, int flags, + Map<String, String> vendorInfo) { + mSelector = selector; + mTuned = tuned; + mStereo = stereo; + mDigital = digital; + mFlags = flags; + mSignalStrength = signalStrength; + mMetadata = metadata; + mVendorInfo = (vendorInfo == null) ? new HashMap<>() : vendorInfo; + } + + /** + * Program selector, necessary for tuning to a program. + * + * @return the program selector. + */ + public @NonNull ProgramSelector getSelector() { + return mSelector; + } + + /** Main channel expressed in units according to band type. + * Currently all defined band types express channels as frequency in kHz + * @return the program channel + * @deprecated Use {@link getSelector()} instead. + */ + @Deprecated + public int getChannel() { + try { + return (int) mSelector.getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY); + } catch (IllegalArgumentException ex) { + Log.w(TAG, "Not an AM/FM program"); + return 0; + } + } + + /** Sub channel ID. E.g 1 for HD radio HD1 + * @return the program sub channel + * @deprecated Use {@link getSelector()} instead. + */ + @Deprecated + public int getSubChannel() { + try { + return (int) mSelector.getFirstId( + ProgramSelector.IDENTIFIER_TYPE_HD_SUBCHANNEL) + 1; + } catch (IllegalArgumentException ex) { + // this is a normal behavior for analog AM/FM selector + return 0; + } + } + + /** {@code true} if the tuner is currently tuned on a valid station + * @return {@code true} if currently tuned, {@code false} otherwise. + */ + public boolean isTuned() { + return mTuned; + } + /** {@code true} if the received program is stereo + * @return {@code true} if stereo, {@code false} otherwise. + */ + public boolean isStereo() { + return mStereo; + } + /** {@code true} if the received program is digital (e.g HD radio) + * @return {@code true} if digital, {@code false} otherwise. + */ + public boolean isDigital() { + return mDigital; + } + + /** + * {@code true} if the program is currently playing live stream. + * This may result in a slightly altered reception parameters, + * usually targetted at reduced latency. + */ + public boolean isLive() { + return (mFlags & FLAG_LIVE) != 0; + } + + /** + * {@code true} if radio stream is not playing, ie. due to bad reception + * conditions or buffering. In this state volume knob MAY be disabled to + * prevent user increasing volume too much. + * It does NOT mean the user has muted audio. + */ + public boolean isMuted() { + return (mFlags & FLAG_MUTED) != 0; + } + + /** + * {@code true} if radio station transmits traffic information + * regularily. + */ + public boolean isTrafficProgram() { + return (mFlags & FLAG_TRAFFIC_PROGRAM) != 0; + } + + /** + * {@code true} if radio station transmits traffic information + * at the very moment. + */ + public boolean isTrafficAnnouncementActive() { + return (mFlags & FLAG_TRAFFIC_ANNOUNCEMENT) != 0; + } + + /** Signal strength indicator from 0 (no signal) to 100 (excellent) + * @return the signal strength indication. + */ + public int getSignalStrength() { + return mSignalStrength; + } + /** Metadata currently received from this station. + * null if no metadata have been received + * @return current meta data received from this program. + */ + public RadioMetadata getMetadata() { + return mMetadata; + } + + /** + * A map of vendor-specific opaque strings, passed from HAL without changes. + * Format of these strings can vary across vendors. + * + * It may be used for extra features, that's not supported by a platform, + * for example: paid-service=true; bitrate=320kbps. + * + * Keys must be prefixed with unique vendor Java-style namespace, + * eg. 'com.somecompany.parameter1'. + */ + public @NonNull Map<String, String> getVendorInfo() { + return mVendorInfo; + } + + private ProgramInfo(Parcel in) { + mSelector = in.readParcelable(null); + mTuned = in.readByte() == 1; + mStereo = in.readByte() == 1; + mDigital = in.readByte() == 1; + mSignalStrength = in.readInt(); + if (in.readByte() == 1) { + mMetadata = RadioMetadata.CREATOR.createFromParcel(in); + } else { + mMetadata = null; + } + mFlags = in.readInt(); + mVendorInfo = readStringMap(in); + } + + public static final Parcelable.Creator<ProgramInfo> CREATOR + = new Parcelable.Creator<ProgramInfo>() { + public ProgramInfo createFromParcel(Parcel in) { + return new ProgramInfo(in); + } + + public ProgramInfo[] newArray(int size) { + return new ProgramInfo[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mSelector, 0); + dest.writeByte((byte)(mTuned ? 1 : 0)); + dest.writeByte((byte)(mStereo ? 1 : 0)); + dest.writeByte((byte)(mDigital ? 1 : 0)); + dest.writeInt(mSignalStrength); + if (mMetadata == null) { + dest.writeByte((byte)0); + } else { + dest.writeByte((byte)1); + mMetadata.writeToParcel(dest, flags); + } + dest.writeInt(mFlags); + writeStringMap(dest, mVendorInfo); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "ProgramInfo [mSelector=" + mSelector + + ", mTuned=" + mTuned + ", mStereo=" + mStereo + ", mDigital=" + mDigital + + ", mFlags=" + mFlags + ", mSignalStrength=" + mSignalStrength + + ((mMetadata == null) ? "" : (", mMetadata=" + mMetadata.toString())) + + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mSelector.hashCode(); + result = prime * result + (mTuned ? 1 : 0); + result = prime * result + (mStereo ? 1 : 0); + result = prime * result + (mDigital ? 1 : 0); + result = prime * result + mFlags; + result = prime * result + mSignalStrength; + result = prime * result + ((mMetadata == null) ? 0 : mMetadata.hashCode()); + result = prime * result + mVendorInfo.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof ProgramInfo)) + return false; + ProgramInfo other = (ProgramInfo) obj; + if (!mSelector.equals(other.getSelector())) return false; + if (mTuned != other.isTuned()) + return false; + if (mStereo != other.isStereo()) + return false; + if (mDigital != other.isDigital()) + return false; + if (mFlags != other.mFlags) + return false; + if (mSignalStrength != other.getSignalStrength()) + return false; + if (mMetadata == null) { + if (other.getMetadata() != null) + return false; + } else if (!mMetadata.equals(other.getMetadata())) + return false; + if (!mVendorInfo.equals(other.mVendorInfo)) return false; + return true; + } + } + + + /** + * Returns a list of descriptors for all broadcast radio modules present on the device. + * @param modules An List of {@link ModuleProperties} where the list will be returned. + * @return + * <ul> + * <li>{@link #STATUS_OK} in case of success, </li> + * <li>{@link #STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link #STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link #STATUS_BAD_VALUE} if modules is null, </li> + * <li>{@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails, </li> + * </ul> + */ + @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO) + public int listModules(List<ModuleProperties> modules) { + if (modules == null) { + Log.e(TAG, "the output list must not be empty"); + return STATUS_BAD_VALUE; + } + + Log.d(TAG, "Listing available tuners..."); + List<ModuleProperties> returnedList; + try { + returnedList = mService.listModules(); + } catch (RemoteException e) { + Log.e(TAG, "Failed listing available tuners", e); + return STATUS_DEAD_OBJECT; + } + + if (returnedList == null) { + Log.e(TAG, "Returned list was a null"); + return STATUS_ERROR; + } + + modules.addAll(returnedList); + return STATUS_OK; + } + + private native int nativeListModules(List<ModuleProperties> modules); + + /** + * Open an interface to control a tuner on a given broadcast radio module. + * Optionally selects and applies the configuration passed as "config" argument. + * @param moduleId radio module identifier {@link ModuleProperties#getId()}. Mandatory. + * @param config desired band and configuration to apply when enabling the hardware module. + * optional, can be null. + * @param withAudio {@code true} to request a tuner with an audio source. + * This tuner is intended for live listening or recording or a radio program. + * If {@code false}, the tuner can only be used to retrieve program informations. + * @param callback {@link RadioTuner.Callback} interface. Mandatory. + * @param handler the Handler on which the callbacks will be received. + * Can be null if default handler is OK. + * @return a valid {@link RadioTuner} interface in case of success or null in case of error. + */ + @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO) + public RadioTuner openTuner(int moduleId, BandConfig config, boolean withAudio, + RadioTuner.Callback callback, Handler handler) { + if (callback == null) { + throw new IllegalArgumentException("callback must not be empty"); + } + + Log.d(TAG, "Opening tuner " + moduleId + "..."); + + ITuner tuner; + TunerCallbackAdapter halCallback = new TunerCallbackAdapter(callback, handler); + try { + tuner = mService.openTuner(moduleId, config, withAudio, halCallback); + } catch (RemoteException e) { + Log.e(TAG, "Failed to open tuner", e); + return null; + } + if (tuner == null) { + Log.e(TAG, "Failed to open tuner"); + return null; + } + return new TunerAdapter(tuner, config != null ? config.getType() : BAND_INVALID); + } + + @NonNull private final Context mContext; + @NonNull private final IRadioService mService; + + /** + * @hide + */ + public RadioManager(@NonNull Context context) throws ServiceNotFoundException { + mContext = context; + mService = IRadioService.Stub.asInterface( + ServiceManager.getServiceOrThrow(Context.RADIO_SERVICE)); + } +} diff --git a/android/hardware/radio/RadioMetadata.java b/android/hardware/radio/RadioMetadata.java new file mode 100644 index 00000000..3cc4b566 --- /dev/null +++ b/android/hardware/radio/RadioMetadata.java @@ -0,0 +1,578 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.hardware.radio; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.Log; +import android.util.SparseArray; + +import java.util.Set; + +/** + * Contains meta data about a radio program such as station name, song title, artist etc... + * @hide + */ +@SystemApi +public final class RadioMetadata implements Parcelable { + private static final String TAG = "BroadcastRadio.metadata"; + + /** + * The RDS Program Information. + */ + public static final String METADATA_KEY_RDS_PI = "android.hardware.radio.metadata.RDS_PI"; + + /** + * The RDS Program Service. + */ + public static final String METADATA_KEY_RDS_PS = "android.hardware.radio.metadata.RDS_PS"; + + /** + * The RDS PTY. + */ + public static final String METADATA_KEY_RDS_PTY = "android.hardware.radio.metadata.RDS_PTY"; + + /** + * The RBDS PTY. + */ + public static final String METADATA_KEY_RBDS_PTY = "android.hardware.radio.metadata.RBDS_PTY"; + + /** + * The RBDS Radio Text. + */ + public static final String METADATA_KEY_RDS_RT = "android.hardware.radio.metadata.RDS_RT"; + + /** + * The song title. + */ + public static final String METADATA_KEY_TITLE = "android.hardware.radio.metadata.TITLE"; + + /** + * The artist name. + */ + public static final String METADATA_KEY_ARTIST = "android.hardware.radio.metadata.ARTIST"; + + /** + * The album name. + */ + public static final String METADATA_KEY_ALBUM = "android.hardware.radio.metadata.ALBUM"; + + /** + * The music genre. + */ + public static final String METADATA_KEY_GENRE = "android.hardware.radio.metadata.GENRE"; + + /** + * The radio station icon {@link Bitmap}. + */ + public static final String METADATA_KEY_ICON = "android.hardware.radio.metadata.ICON"; + + /** + * The artwork for the song/album {@link Bitmap}. + */ + public static final String METADATA_KEY_ART = "android.hardware.radio.metadata.ART"; + + /** + * The clock. + */ + public static final String METADATA_KEY_CLOCK = "android.hardware.radio.metadata.CLOCK"; + + + private static final int METADATA_TYPE_INVALID = -1; + private static final int METADATA_TYPE_INT = 0; + private static final int METADATA_TYPE_TEXT = 1; + private static final int METADATA_TYPE_BITMAP = 2; + private static final int METADATA_TYPE_CLOCK = 3; + + private static final ArrayMap<String, Integer> METADATA_KEYS_TYPE; + + static { + METADATA_KEYS_TYPE = new ArrayMap<String, Integer>(); + METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PI, METADATA_TYPE_INT); + METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PS, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_PTY, METADATA_TYPE_INT); + METADATA_KEYS_TYPE.put(METADATA_KEY_RBDS_PTY, METADATA_TYPE_INT); + METADATA_KEYS_TYPE.put(METADATA_KEY_RDS_RT, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ICON, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_CLOCK, METADATA_TYPE_CLOCK); + } + + // keep in sync with: system/media/radio/include/system/radio_metadata.h + private static final int NATIVE_KEY_INVALID = -1; + private static final int NATIVE_KEY_RDS_PI = 0; + private static final int NATIVE_KEY_RDS_PS = 1; + private static final int NATIVE_KEY_RDS_PTY = 2; + private static final int NATIVE_KEY_RBDS_PTY = 3; + private static final int NATIVE_KEY_RDS_RT = 4; + private static final int NATIVE_KEY_TITLE = 5; + private static final int NATIVE_KEY_ARTIST = 6; + private static final int NATIVE_KEY_ALBUM = 7; + private static final int NATIVE_KEY_GENRE = 8; + private static final int NATIVE_KEY_ICON = 9; + private static final int NATIVE_KEY_ART = 10; + private static final int NATIVE_KEY_CLOCK = 11; + + private static final SparseArray<String> NATIVE_KEY_MAPPING; + + static { + NATIVE_KEY_MAPPING = new SparseArray<String>(); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PI, METADATA_KEY_RDS_PI); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PS, METADATA_KEY_RDS_PS); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_PTY, METADATA_KEY_RDS_PTY); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_RBDS_PTY, METADATA_KEY_RBDS_PTY); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_RDS_RT, METADATA_KEY_RDS_RT); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_TITLE, METADATA_KEY_TITLE); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_ARTIST, METADATA_KEY_ARTIST); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_ALBUM, METADATA_KEY_ALBUM); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_GENRE, METADATA_KEY_GENRE); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_ICON, METADATA_KEY_ICON); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_ART, METADATA_KEY_ART); + NATIVE_KEY_MAPPING.put(NATIVE_KEY_CLOCK, METADATA_KEY_CLOCK); + } + + /** + * Provides a Clock that can be used to describe time as provided by the Radio. + * + * The clock is defined by the seconds since epoch at the UTC + 0 timezone + * and timezone offset from UTC + 0 represented in number of minutes. + * + * @hide + */ + @SystemApi + public static final class Clock implements Parcelable { + private final long mUtcEpochSeconds; + private final int mTimezoneOffsetMinutes; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeLong(mUtcEpochSeconds); + out.writeInt(mTimezoneOffsetMinutes); + } + + public static final Parcelable.Creator<Clock> CREATOR + = new Parcelable.Creator<Clock>() { + public Clock createFromParcel(Parcel in) { + return new Clock(in); + } + + public Clock[] newArray(int size) { + return new Clock[size]; + } + }; + + public Clock(long utcEpochSeconds, int timezoneOffsetMinutes) { + mUtcEpochSeconds = utcEpochSeconds; + mTimezoneOffsetMinutes = timezoneOffsetMinutes; + } + + private Clock(Parcel in) { + mUtcEpochSeconds = in.readLong(); + mTimezoneOffsetMinutes = in.readInt(); + } + + public long getUtcEpochSeconds() { + return mUtcEpochSeconds; + } + + public int getTimezoneOffsetMinutes() { + return mTimezoneOffsetMinutes; + } + } + + private final Bundle mBundle; + + RadioMetadata() { + mBundle = new Bundle(); + } + + private RadioMetadata(Bundle bundle) { + mBundle = new Bundle(bundle); + } + + private RadioMetadata(Parcel in) { + mBundle = in.readBundle(); + } + + /** + * Returns {@code true} if the given key is contained in the meta data + * + * @param key a String key + * @return {@code true} if the key exists in this meta data, {@code false} otherwise + */ + public boolean containsKey(String key) { + return mBundle.containsKey(key); + } + + /** + * Returns the text value associated with the given key as a String, or null + * if the key is not found in the meta data. + * + * @param key The key the value is stored under + * @return a String value, or null + */ + public String getString(String key) { + return mBundle.getString(key); + } + + private static void putInt(Bundle bundle, String key, int value) { + int type = METADATA_KEYS_TYPE.getOrDefault(key, METADATA_TYPE_INVALID); + if (type != METADATA_TYPE_INT && type != METADATA_TYPE_BITMAP) { + throw new IllegalArgumentException("The " + key + " key cannot be used to put an int"); + } + bundle.putInt(key, value); + } + + /** + * Returns the value associated with the given key, + * or 0 if the key is not found in the meta data. + * + * @param key The key the value is stored under + * @return an int value + */ + public int getInt(String key) { + return mBundle.getInt(key, 0); + } + + /** + * Returns a {@link Bitmap} for the given key or null if the key is not found in the meta data. + * + * @param key The key the value is stored under + * @return a {@link Bitmap} or null + * @deprecated Use getBitmapId(String) instead + */ + @Deprecated + public Bitmap getBitmap(String key) { + Bitmap bmp = null; + try { + bmp = mBundle.getParcelable(key); + } catch (Exception e) { + // ignore, value was not a bitmap + Log.w(TAG, "Failed to retrieve a key as Bitmap.", e); + } + return bmp; + } + + /** + * Retrieves an identifier for a bitmap. + * + * The format of an identifier is opaque to the application, + * with a special case of value 0 being invalid. + * An identifier for a given image-tuner pair is unique, so an application + * may cache images and determine if there is a necessity to fetch them + * again - if identifier changes, it means the image has changed. + * + * Only bitmap keys may be used with this method: + * <ul> + * <li>{@link #METADATA_KEY_ICON}</li> + * <li>{@link #METADATA_KEY_ART}</li> + * </ul> + * + * @param key The key the value is stored under. + * @return a bitmap identifier or 0 if it's missing. + * @hide This API is not thoroughly elaborated yet + */ + public int getBitmapId(@NonNull String key) { + if (!METADATA_KEY_ICON.equals(key) && !METADATA_KEY_ART.equals(key)) return 0; + return getInt(key); + } + + public Clock getClock(String key) { + Clock clock = null; + try { + clock = mBundle.getParcelable(key); + } catch (Exception e) { + // ignore, value was not a clock. + Log.w(TAG, "Failed to retrieve a key as Clock.", e); + } + return clock; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBundle(mBundle); + } + + /** + * Returns the number of fields in this meta data. + * + * @return the number of fields in the meta data. + */ + public int size() { + return mBundle.size(); + } + + /** + * Returns a Set containing the Strings used as keys in this meta data. + * + * @return a Set of String keys + */ + public Set<String> keySet() { + return mBundle.keySet(); + } + + /** + * Helper for getting the String key used by {@link RadioMetadata} from the + * corrsponding native integer key. + * + * @param editorKey The key used by the editor + * @return the key used by this class or null if no mapping exists + * @hide + */ + public static String getKeyFromNativeKey(int nativeKey) { + return NATIVE_KEY_MAPPING.get(nativeKey, null); + } + + public static final Parcelable.Creator<RadioMetadata> CREATOR = + new Parcelable.Creator<RadioMetadata>() { + @Override + public RadioMetadata createFromParcel(Parcel in) { + return new RadioMetadata(in); + } + + @Override + public RadioMetadata[] newArray(int size) { + return new RadioMetadata[size]; + } + }; + + /** + * Use to build RadioMetadata objects. + */ + public static final class Builder { + private final Bundle mBundle; + + /** + * Create an empty Builder. Any field that should be included in the + * {@link RadioMetadata} must be added. + */ + public Builder() { + mBundle = new Bundle(); + } + + /** + * Create a Builder using a {@link RadioMetadata} instance to set the + * initial values. All fields in the source meta data will be included in + * the new meta data. Fields can be overwritten by adding the same key. + * + * @param source + */ + public Builder(RadioMetadata source) { + mBundle = new Bundle(source.mBundle); + } + + /** + * Create a Builder using a {@link RadioMetadata} instance to set + * initial values, but replace bitmaps with a scaled down copy if they + * are larger than maxBitmapSize. + * + * @param source The original meta data to copy. + * @param maxBitmapSize The maximum height/width for bitmaps contained + * in the meta data. + * @hide + */ + public Builder(RadioMetadata source, int maxBitmapSize) { + this(source); + for (String key : mBundle.keySet()) { + Object value = mBundle.get(key); + if (value != null && value instanceof Bitmap) { + Bitmap bmp = (Bitmap) value; + if (bmp.getHeight() > maxBitmapSize || bmp.getWidth() > maxBitmapSize) { + putBitmap(key, scaleBitmap(bmp, maxBitmapSize)); + } + } + } + } + + /** + * Put a String value into the meta data. Custom keys may be used, but if + * the METADATA_KEYs defined in this class are used they may only be one + * of the following: + * <ul> + * <li>{@link #METADATA_KEY_RDS_PS}</li> + * <li>{@link #METADATA_KEY_RDS_RT}</li> + * <li>{@link #METADATA_KEY_TITLE}</li> + * <li>{@link #METADATA_KEY_ARTIST}</li> + * <li>{@link #METADATA_KEY_ALBUM}</li> + * <li>{@link #METADATA_KEY_GENRE}</li> + * </ul> + * + * @param key The key for referencing this value + * @param value The String value to store + * @return the same Builder instance + */ + public Builder putString(String key, String value) { + if (!METADATA_KEYS_TYPE.containsKey(key) || + METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a String"); + } + mBundle.putString(key, value); + return this; + } + + /** + * Put an int value into the meta data. Custom keys may be used, but if + * the METADATA_KEYs defined in this class are used they may only be one + * of the following: + * <ul> + * <li>{@link #METADATA_KEY_RDS_PI}</li> + * <li>{@link #METADATA_KEY_RDS_PTY}</li> + * <li>{@link #METADATA_KEY_RBDS_PTY}</li> + * </ul> + * or any bitmap represented by its identifier. + * + * @param key The key for referencing this value + * @param value The int value to store + * @return the same Builder instance + */ + public Builder putInt(String key, int value) { + RadioMetadata.putInt(mBundle, key, value); + return this; + } + + /** + * Put a {@link Bitmap} into the meta data. Custom keys may be used, but + * if the METADATA_KEYs defined in this class are used they may only be + * one of the following: + * <ul> + * <li>{@link #METADATA_KEY_ICON}</li> + * <li>{@link #METADATA_KEY_ART}</li> + * </ul> + * <p> + * + * @param key The key for referencing this value + * @param value The Bitmap to store + * @return the same Builder instance + */ + public Builder putBitmap(String key, Bitmap value) { + if (!METADATA_KEYS_TYPE.containsKey(key) || + METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a Bitmap"); + } + mBundle.putParcelable(key, value); + return this; + } + + /** + * Put a {@link RadioMetadata.Clock} into the meta data. Custom keys may be used, but if the + * METADATA_KEYs defined in this class are used they may only be one of the following: + * <ul> + * <li>{@link #MEADATA_KEY_CLOCK}</li> + * </ul> + * + * @param utcSecondsSinceEpoch Number of seconds since epoch for UTC + 0 timezone. + * @param timezoneOffsetInMinutes Offset of timezone from UTC + 0 in minutes. + * @return the same Builder instance. + */ + public Builder putClock(String key, long utcSecondsSinceEpoch, int timezoneOffsetMinutes) { + if (!METADATA_KEYS_TYPE.containsKey(key) || + METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_CLOCK) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a RadioMetadata.Clock."); + } + mBundle.putParcelable(key, new Clock(utcSecondsSinceEpoch, timezoneOffsetMinutes)); + return this; + } + + /** + * Creates a {@link RadioMetadata} instance with the specified fields. + * + * @return a new {@link RadioMetadata} object + */ + public RadioMetadata build() { + return new RadioMetadata(mBundle); + } + + private Bitmap scaleBitmap(Bitmap bmp, int maxSize) { + float maxSizeF = maxSize; + float widthScale = maxSizeF / bmp.getWidth(); + float heightScale = maxSizeF / bmp.getHeight(); + float scale = Math.min(widthScale, heightScale); + int height = (int) (bmp.getHeight() * scale); + int width = (int) (bmp.getWidth() * scale); + return Bitmap.createScaledBitmap(bmp, width, height, true); + } + } + + int putIntFromNative(int nativeKey, int value) { + String key = getKeyFromNativeKey(nativeKey); + try { + putInt(mBundle, key, value); + return 0; + } catch (IllegalArgumentException ex) { + return -1; + } + } + + int putStringFromNative(int nativeKey, String value) { + String key = getKeyFromNativeKey(nativeKey); + if (!METADATA_KEYS_TYPE.containsKey(key) || + METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) { + return -1; + } + mBundle.putString(key, value); + return 0; + } + + int putBitmapFromNative(int nativeKey, byte[] value) { + String key = getKeyFromNativeKey(nativeKey); + if (!METADATA_KEYS_TYPE.containsKey(key) || + METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) { + return -1; + } + Bitmap bmp = null; + try { + bmp = BitmapFactory.decodeByteArray(value, 0, value.length); + if (bmp != null) { + mBundle.putParcelable(key, bmp); + return 0; + } + } catch (Exception e) { + } + return -1; + } + + int putClockFromNative(int nativeKey, long utcEpochSeconds, int timezoneOffsetInMinutes) { + String key = getKeyFromNativeKey(nativeKey); + if (!METADATA_KEYS_TYPE.containsKey(key) || + METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_CLOCK) { + return -1; + } + mBundle.putParcelable(key, new RadioMetadata.Clock( + utcEpochSeconds, timezoneOffsetInMinutes)); + return 0; + } +} diff --git a/android/hardware/radio/RadioTuner.java b/android/hardware/radio/RadioTuner.java new file mode 100644 index 00000000..6e8991aa --- /dev/null +++ b/android/hardware/radio/RadioTuner.java @@ -0,0 +1,435 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.radio; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.graphics.Bitmap; +import android.os.Handler; + +import java.util.List; +import java.util.Map; + +/** + * RadioTuner interface provides methods to control a radio tuner on the device: selecting and + * configuring the active band, muting/unmuting, scanning and tuning, etc... + * + * Obtain a RadioTuner interface by calling {@link RadioManager#openTuner(int, + * RadioManager.BandConfig, boolean, RadioTuner.Callback, Handler)}. + * @hide + */ +@SystemApi +public abstract class RadioTuner { + + /** Scanning direction UP for {@link #step(int, boolean)}, {@link #scan(int, boolean)} */ + public static final int DIRECTION_UP = 0; + + /** Scanning directions DOWN for {@link #step(int, boolean)}, {@link #scan(int, boolean)} */ + public static final int DIRECTION_DOWN = 1; + + /** + * Close the tuner interface. The {@link Callback} callback will not be called + * anymore and associated resources will be released. + * Must be called when the tuner is not needed to make hardware resources available to others. + * */ + public abstract void close(); + + /** + * Set the active band configuration for this module. + * Must be a valid configuration obtained via buildConfig() from a valid BandDescriptor listed + * in the ModuleProperties of the module with the specified ID. + * @param config The desired band configuration (FmBandConfig or AmBandConfig). + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + */ + public abstract int setConfiguration(RadioManager.BandConfig config); + + /** + * Get current configuration. + * @param config a BandConfig array of lengh 1 where the configuration is returned. + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + */ + public abstract int getConfiguration(RadioManager.BandConfig[] config); + + + /** + * Set mute state. When muted, the radio tuner audio source is not available for playback on + * any audio device. when unmuted, the radio tuner audio source is output as a media source + * and renderd over the audio device selected for media use case. + * The radio tuner audio source is muted by default when the tuner is first attached. + * Only effective if the tuner is attached with audio enabled. + * + * @param mute the requested mute state. + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + */ + public abstract int setMute(boolean mute); + + /** + * Get mute state. + * + * @return {@code true} if the radio tuner audio source is muted or a problem occured + * retrieving the mute state, {@code false} otherwise. + */ + public abstract boolean getMute(); + + /** + * Step up or down by one channel spacing. + * The operation is asynchronous and {@link Callback} + * onProgramInfoChanged() will be called when step completes or + * onError() when cancelled or timeout. + * @param direction {@link #DIRECTION_UP} or {@link #DIRECTION_DOWN}. + * @param skipSubChannel indicates to skip sub channels when the configuration currently + * selected supports sub channel (e.g HD Radio). N/A otherwise. + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + */ + public abstract int step(int direction, boolean skipSubChannel); + + /** + * Scan up or down to next valid station. + * The operation is asynchronous and {@link Callback} + * onProgramInfoChanged() will be called when scan completes or + * onError() when cancelled or timeout. + * @param direction {@link #DIRECTION_UP} or {@link #DIRECTION_DOWN}. + * @param skipSubChannel indicates to skip sub channels when the configuration currently + * selected supports sub channel (e.g HD Radio). N/A otherwise. + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + */ + public abstract int scan(int direction, boolean skipSubChannel); + + /** + * Tune to a specific frequency. + * The operation is asynchronous and {@link Callback} + * onProgramInfoChanged() will be called when tune completes or + * onError() when cancelled or timeout. + * @param channel the specific channel or frequency to tune to. + * @param subChannel the specific sub-channel to tune to. N/A if the selected configuration + * does not support cub channels. + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + * @deprecated Use {@link tune(ProgramSelector)} instead. + */ + @Deprecated + public abstract int tune(int channel, int subChannel); + + /** + * Tune to a program. + * + * The operation is asynchronous and {@link Callback} onProgramInfoChanged() will be called + * when tune completes or onError() when cancelled or on timeout. + * + * @throws IllegalArgumentException if the provided selector is invalid + */ + public abstract void tune(@NonNull ProgramSelector selector); + + /** + * Cancel a pending scan or tune operation. + * If an operation is pending, {@link Callback} onError() will be called with + * {@link #ERROR_CANCELLED}. + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + */ + public abstract int cancel(); + + /** + * Cancels traffic or emergency announcement. + * + * If there was no announcement to cancel, no action is taken. + * + * There is a race condition between calling cancelAnnouncement and the actual announcement + * being finished, so onTrafficAnnouncement / onEmergencyAnnouncement callback should be + * tracked with proper locking. + */ + public abstract void cancelAnnouncement(); + + /** + * Get current station information. + * @param info a ProgramInfo array of lengh 1 where the information is returned. + * @return + * <ul> + * <li>{@link RadioManager#STATUS_OK} in case of success, </li> + * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li> + * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li> + * <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li> + * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li> + * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails, </li> + * </ul> + */ + public abstract int getProgramInformation(RadioManager.ProgramInfo[] info); + + /** + * Retrieves a {@link Bitmap} for the given image ID or null, + * if the image was missing from the tuner. + * + * This involves doing a call to the tuner, so the bitmap should be cached + * on the application side. + * + * If the method returns null for non-zero ID, it means the image was + * updated on the tuner side. There is a race conditon between fetching + * image for an old ID and tuner updating the image (and cleaning up the + * old image). In such case, a new ProgramInfo with updated image id will + * be sent with a {@link onProgramInfoChanged} callback. + * + * @param id The image identifier, retrieved with + * {@link RadioMetadata#getBitmapId(String)}. + * @return A {@link Bitmap} or null. + * @throws IllegalArgumentException if id==0 + * @hide This API is not thoroughly elaborated yet + */ + public abstract @Nullable Bitmap getMetadataImage(int id); + + /** + * Initiates a background scan to update internally cached program list. + * + * It may not be necessary to initiate the scan explicitly - the scan MAY be performed on boot. + * + * The operation is asynchronous and {@link Callback} backgroundScanComplete or onError will + * be called if the return value of this call was {@code true}. As result of this call + * programListChanged may be triggered (if the scanned list differs). + * + * @return {@code true} if the scan was properly scheduled, {@code false} if the scan feature + * is unavailable; ie. temporarily due to ongoing foreground playback in single-tuner device + * or permanently if the feature is not supported + * (see ModuleProperties#isBackgroundScanningSupported()). + */ + public abstract boolean startBackgroundScan(); + + /** + * Get the list of discovered radio stations. + * + * To get the full list, set filter to null or empty map. + * Keys must be prefixed with unique vendor Java-style namespace, + * eg. 'com.somecompany.parameter1'. + * + * @param vendorFilter vendor-specific selector for radio stations. + * @return a list of radio stations. + * @throws IllegalStateException if the scan is in progress or has not been started, + * startBackgroundScan() call may fix it. + * @throws IllegalArgumentException if the vendorFilter argument is not valid. + */ + public abstract @NonNull List<RadioManager.ProgramInfo> + getProgramList(@Nullable Map<String, String> vendorFilter); + + /** + * Checks, if the analog playback is forced, see setAnalogForced. + * + * @throws IllegalStateException if the switch is not supported at current + * configuration. + * @return {@code true} if analog is forced, {@code false} otherwise. + */ + public abstract boolean isAnalogForced(); + + /** + * Forces the analog playback for the supporting radio technology. + * + * User may disable digital playback for FM HD Radio or hybrid FM/DAB with + * this option. This is purely user choice, ie. does not reflect digital- + * analog handover managed from the HAL implementation side. + * + * Some radio technologies may not support this, ie. DAB. + * + * @param isForced {@code true} to force analog, {@code false} for a default behaviour. + * @throws IllegalStateException if the switch is not supported at current + * configuration. + */ + public abstract void setAnalogForced(boolean isForced); + + /** + * Get current antenna connection state for current configuration. + * Only valid if a configuration has been applied. + * @return {@code true} if the antenna is connected, {@code false} otherwise. + */ + public abstract boolean isAntennaConnected(); + + /** + * Indicates if this client actually controls the tuner. + * Control is always granted after + * {@link RadioManager#openTuner(int, + * RadioManager.BandConfig, boolean, Callback, Handler)} + * returns a non null tuner interface. + * Control is lost when another client opens an interface on the same tuner. + * When this happens, {@link Callback#onControlChanged(boolean)} is received. + * The client can either wait for control to be returned (which is indicated by the same + * callback) or close and reopen the tuner interface. + * @return {@code true} if this interface controls the tuner, + * {@code false} otherwise or if a problem occured retrieving the state. + */ + public abstract boolean hasControl(); + + /** Indicates a failure of radio IC or driver. + * The application must close and re open the tuner */ + public static final int ERROR_HARDWARE_FAILURE = 0; + /** Indicates a failure of the radio service. + * The application must close and re open the tuner */ + public static final int ERROR_SERVER_DIED = 1; + /** A pending seek or tune operation was cancelled */ + public static final int ERROR_CANCELLED = 2; + /** A pending seek or tune operation timed out */ + public static final int ERROR_SCAN_TIMEOUT = 3; + /** The requested configuration could not be applied */ + public static final int ERROR_CONFIG = 4; + /** Background scan was interrupted due to hardware becoming temporarily unavailable. */ + public static final int ERROR_BACKGROUND_SCAN_UNAVAILABLE = 5; + /** Background scan failed due to other error, ie. HW failure. */ + public static final int ERROR_BACKGROUND_SCAN_FAILED = 6; + + /** + * Callback provided by the client application when opening a {@link RadioTuner} + * to receive asynchronous operation results, updates and error notifications. + */ + public static abstract class Callback { + /** + * onError() is called when an error occured while performing an asynchronous + * operation of when the hardware or system service experiences a problem. + * status is one of {@link #ERROR_HARDWARE_FAILURE}, {@link #ERROR_SERVER_DIED}, + * {@link #ERROR_CANCELLED}, {@link #ERROR_SCAN_TIMEOUT}, + * {@link #ERROR_CONFIG} + */ + public void onError(int status) {} + /** + * onConfigurationChanged() is called upon successful completion of + * {@link RadioManager#openTuner(int, RadioManager.BandConfig, boolean, Callback, Handler)} + * or {@link RadioTuner#setConfiguration(RadioManager.BandConfig)} + */ + public void onConfigurationChanged(RadioManager.BandConfig config) {} + + /** + * Called when program info (including metadata) for the current program has changed. + * + * It happens either upon successful completion of {@link RadioTuner#step(int, boolean)}, + * {@link RadioTuner#scan(int, boolean)}, {@link RadioTuner#tune(int, int)}; when + * a switching to alternate frequency occurs; or when metadata is updated. + */ + public void onProgramInfoChanged(RadioManager.ProgramInfo info) {} + + /** + * Called when metadata is updated for the current program. + * + * @deprecated Use {@link #onProgramInfoChanged(RadioManager.ProgramInfo)} instead. + */ + @Deprecated + public void onMetadataChanged(RadioMetadata metadata) {} + + /** + * onTrafficAnnouncement() is called when a traffic announcement starts and stops. + */ + public void onTrafficAnnouncement(boolean active) {} + /** + * onEmergencyAnnouncement() is called when an emergency annoucement starts and stops. + */ + public void onEmergencyAnnouncement(boolean active) {} + /** + * onAntennaState() is called when the antenna is connected or disconnected. + */ + public void onAntennaState(boolean connected) {} + /** + * onControlChanged() is called when the client loses or gains control of the radio tuner. + * The control is always granted after a successful call to + * {@link RadioManager#openTuner(int, RadioManager.BandConfig, boolean, Callback, Handler)}. + * If another client opens the same tuner, onControlChanged() will be called with + * control set to {@code false} to indicate loss of control. + * At this point, RadioTuner APIs other than getters will return + * {@link RadioManager#STATUS_INVALID_OPERATION}. + * When the other client releases the tuner, onControlChanged() will be called + * with control set to {@code true}. + */ + public void onControlChanged(boolean control) {} + + /** + * onBackgroundScanAvailabilityChange() is called when background scan + * feature becomes available or not. + * + * @param isAvailable true, if the tuner turned temporarily background- + * capable, false in the other case. + */ + public void onBackgroundScanAvailabilityChange(boolean isAvailable) {} + + /** + * Called when a background scan completes successfully. + */ + public void onBackgroundScanComplete() {} + + /** + * Called when available program list changed. + * + * Use {@link RadioTuner#getProgramList(String)} to get an actual list. + */ + public void onProgramListChanged() {} + } + +} + diff --git a/android/hardware/radio/TunerAdapter.java b/android/hardware/radio/TunerAdapter.java new file mode 100644 index 00000000..b6219690 --- /dev/null +++ b/android/hardware/radio/TunerAdapter.java @@ -0,0 +1,271 @@ +/** + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.radio; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Bitmap; +import android.os.RemoteException; +import android.util.Log; + +import java.util.List; +import java.util.Map; + +/** + * Implements the RadioTuner interface by forwarding calls to radio service. + */ +class TunerAdapter extends RadioTuner { + private static final String TAG = "BroadcastRadio.TunerAdapter"; + + @NonNull private final ITuner mTuner; + private boolean mIsClosed = false; + + private @RadioManager.Band int mBand; + + TunerAdapter(ITuner tuner, @RadioManager.Band int band) { + if (tuner == null) { + throw new NullPointerException(); + } + mTuner = tuner; + mBand = band; + } + + @Override + public void close() { + synchronized (mTuner) { + if (mIsClosed) { + Log.v(TAG, "Tuner is already closed"); + return; + } + mIsClosed = true; + } + try { + mTuner.close(); + } catch (RemoteException e) { + Log.e(TAG, "Exception trying to close tuner", e); + } + } + + @Override + public int setConfiguration(RadioManager.BandConfig config) { + try { + mTuner.setConfiguration(config); + mBand = config.getType(); + return RadioManager.STATUS_OK; + } catch (IllegalArgumentException e) { + Log.e(TAG, "Can't set configuration", e); + return RadioManager.STATUS_BAD_VALUE; + } catch (RemoteException e) { + Log.e(TAG, "service died", e); + return RadioManager.STATUS_DEAD_OBJECT; + } + } + + @Override + public int getConfiguration(RadioManager.BandConfig[] config) { + if (config == null || config.length != 1) { + throw new IllegalArgumentException("The argument must be an array of length 1"); + } + try { + config[0] = mTuner.getConfiguration(); + return RadioManager.STATUS_OK; + } catch (RemoteException e) { + Log.e(TAG, "service died", e); + return RadioManager.STATUS_DEAD_OBJECT; + } + } + + @Override + public int setMute(boolean mute) { + try { + mTuner.setMuted(mute); + } catch (IllegalStateException e) { + Log.e(TAG, "Can't set muted", e); + return RadioManager.STATUS_ERROR; + } catch (RemoteException e) { + Log.e(TAG, "service died", e); + return RadioManager.STATUS_DEAD_OBJECT; + } + return RadioManager.STATUS_OK; + } + + @Override + public boolean getMute() { + try { + return mTuner.isMuted(); + } catch (RemoteException e) { + Log.e(TAG, "service died", e); + return true; + } + } + + @Override + public int step(int direction, boolean skipSubChannel) { + try { + mTuner.step(direction == RadioTuner.DIRECTION_DOWN, skipSubChannel); + } catch (IllegalStateException e) { + Log.e(TAG, "Can't step", e); + return RadioManager.STATUS_INVALID_OPERATION; + } catch (RemoteException e) { + Log.e(TAG, "service died", e); + return RadioManager.STATUS_DEAD_OBJECT; + } + return RadioManager.STATUS_OK; + } + + @Override + public int scan(int direction, boolean skipSubChannel) { + try { + mTuner.scan(direction == RadioTuner.DIRECTION_DOWN, skipSubChannel); + } catch (IllegalStateException e) { + Log.e(TAG, "Can't scan", e); + return RadioManager.STATUS_INVALID_OPERATION; + } catch (RemoteException e) { + Log.e(TAG, "service died", e); + return RadioManager.STATUS_DEAD_OBJECT; + } + return RadioManager.STATUS_OK; + } + + @Override + public int tune(int channel, int subChannel) { + try { + mTuner.tune(ProgramSelector.createAmFmSelector(mBand, channel, subChannel)); + } catch (IllegalStateException e) { + Log.e(TAG, "Can't tune", e); + return RadioManager.STATUS_INVALID_OPERATION; + } catch (IllegalArgumentException e) { + Log.e(TAG, "Can't tune", e); + return RadioManager.STATUS_BAD_VALUE; + } catch (RemoteException e) { + Log.e(TAG, "service died", e); + return RadioManager.STATUS_DEAD_OBJECT; + } + return RadioManager.STATUS_OK; + } + + @Override + public void tune(@NonNull ProgramSelector selector) { + try { + mTuner.tune(selector); + } catch (RemoteException e) { + throw new RuntimeException("service died", e); + } + } + + @Override + public int cancel() { + try { + mTuner.cancel(); + } catch (IllegalStateException e) { + Log.e(TAG, "Can't cancel", e); + return RadioManager.STATUS_INVALID_OPERATION; + } catch (RemoteException e) { + Log.e(TAG, "service died", e); + return RadioManager.STATUS_DEAD_OBJECT; + } + return RadioManager.STATUS_OK; + } + + @Override + public void cancelAnnouncement() { + try { + mTuner.cancelAnnouncement(); + } catch (RemoteException e) { + throw new RuntimeException("service died", e); + } + } + + @Override + public int getProgramInformation(RadioManager.ProgramInfo[] info) { + if (info == null || info.length != 1) { + throw new IllegalArgumentException("The argument must be an array of length 1"); + } + try { + info[0] = mTuner.getProgramInformation(); + return RadioManager.STATUS_OK; + } catch (RemoteException e) { + Log.e(TAG, "service died", e); + return RadioManager.STATUS_DEAD_OBJECT; + } + } + + @Override + public @Nullable Bitmap getMetadataImage(int id) { + try { + return mTuner.getImage(id); + } catch (RemoteException e) { + throw new RuntimeException("service died", e); + } + } + + @Override + public boolean startBackgroundScan() { + try { + return mTuner.startBackgroundScan(); + } catch (RemoteException e) { + throw new RuntimeException("service died", e); + } + } + + @Override + public @NonNull List<RadioManager.ProgramInfo> + getProgramList(@Nullable Map<String, String> vendorFilter) { + try { + return mTuner.getProgramList(vendorFilter); + } catch (RemoteException e) { + throw new RuntimeException("service died", e); + } + } + + @Override + public boolean isAnalogForced() { + try { + return mTuner.isAnalogForced(); + } catch (RemoteException e) { + throw new RuntimeException("service died", e); + } + } + + @Override + public void setAnalogForced(boolean isForced) { + try { + mTuner.setAnalogForced(isForced); + } catch (RemoteException e) { + throw new RuntimeException("service died", e); + } + } + + @Override + public boolean isAntennaConnected() { + try { + return mTuner.isAntennaConnected(); + } catch (RemoteException e) { + throw new RuntimeException("service died", e); + } + } + + @Override + public boolean hasControl() { + try { + // don't rely on mIsClosed, as tuner might get closed internally + return !mTuner.isClosed(); + } catch (RemoteException e) { + return false; + } + } +} diff --git a/android/hardware/radio/TunerCallbackAdapter.java b/android/hardware/radio/TunerCallbackAdapter.java new file mode 100644 index 00000000..ffd5b30f --- /dev/null +++ b/android/hardware/radio/TunerCallbackAdapter.java @@ -0,0 +1,97 @@ +/** + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.radio; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +/** + * Implements the ITunerCallback interface by forwarding calls to RadioTuner.Callback. + */ +class TunerCallbackAdapter extends ITunerCallback.Stub { + private static final String TAG = "BroadcastRadio.TunerCallbackAdapter"; + + @NonNull private final RadioTuner.Callback mCallback; + @NonNull private final Handler mHandler; + + TunerCallbackAdapter(@NonNull RadioTuner.Callback callback, @Nullable Handler handler) { + mCallback = callback; + if (handler == null) { + mHandler = new Handler(Looper.getMainLooper()); + } else { + mHandler = handler; + } + } + + @Override + public void onError(int status) { + mHandler.post(() -> mCallback.onError(status)); + } + + @Override + public void onConfigurationChanged(RadioManager.BandConfig config) { + mHandler.post(() -> mCallback.onConfigurationChanged(config)); + } + + @Override + public void onCurrentProgramInfoChanged(RadioManager.ProgramInfo info) { + if (info == null) { + Log.e(TAG, "ProgramInfo must not be null"); + return; + } + + mHandler.post(() -> { + mCallback.onProgramInfoChanged(info); + + RadioMetadata metadata = info.getMetadata(); + if (metadata != null) mCallback.onMetadataChanged(metadata); + }); + } + + @Override + public void onTrafficAnnouncement(boolean active) { + mHandler.post(() -> mCallback.onTrafficAnnouncement(active)); + } + + @Override + public void onEmergencyAnnouncement(boolean active) { + mHandler.post(() -> mCallback.onEmergencyAnnouncement(active)); + } + + @Override + public void onAntennaState(boolean connected) { + mHandler.post(() -> mCallback.onAntennaState(connected)); + } + + @Override + public void onBackgroundScanAvailabilityChange(boolean isAvailable) { + mHandler.post(() -> mCallback.onBackgroundScanAvailabilityChange(isAvailable)); + } + + @Override + public void onBackgroundScanComplete() { + mHandler.post(() -> mCallback.onBackgroundScanComplete()); + } + + @Override + public void onProgramListChanged() { + mHandler.post(() -> mCallback.onProgramListChanged()); + } +} |