diff --git a/Android.mk b/Android.mk index 2d8739b5384fd..cca87caa6bd75 100644 --- a/Android.mk +++ b/Android.mk @@ -712,6 +712,7 @@ aidl_files := \ frameworks/base/core/java/android/print/PrinterInfo.aidl \ frameworks/base/core/java/android/print/PrintJobId.aidl \ frameworks/base/core/java/android/printservice/recommendation/RecommendationInfo.aidl \ + frameworks/base/core/java/android/hardware/radio/ProgramSelector.aidl \ frameworks/base/core/java/android/hardware/radio/RadioManager.aidl \ frameworks/base/core/java/android/hardware/radio/RadioMetadata.aidl \ frameworks/base/core/java/android/hardware/usb/UsbDevice.aidl \ diff --git a/api/system-current.txt b/api/system-current.txt index 678a4cda19a9a..d176e39281737 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -17215,6 +17215,63 @@ package android.hardware.location { package android.hardware.radio { + public final class ProgramSelector implements android.os.Parcelable { + ctor public ProgramSelector(int, android.hardware.radio.ProgramSelector.Identifier, android.hardware.radio.ProgramSelector.Identifier[], long[]); + method public static android.hardware.radio.ProgramSelector createAmFmSelector(int, int); + method public static android.hardware.radio.ProgramSelector createAmFmSelector(int, int, int); + method public int describeContents(); + method public android.hardware.radio.ProgramSelector.Identifier[] getAllIds(int); + method public long getFirstId(int); + method public android.hardware.radio.ProgramSelector.Identifier getPrimaryId(); + method public int getProgramType(); + method public android.hardware.radio.ProgramSelector.Identifier[] getSecondaryIds(); + method public long[] getVendorIds(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int IDENTIFIER_TYPE_AMFM_FREQUENCY = 1; // 0x1 + field public static final int IDENTIFIER_TYPE_DAB_ENSEMBLE = 6; // 0x6 + field public static final int IDENTIFIER_TYPE_DAB_FREQUENCY = 8; // 0x8 + field public static final int IDENTIFIER_TYPE_DAB_SCID = 7; // 0x7 + field public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5; // 0x5 + field public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10; // 0xa + field public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9; // 0x9 + field public static final int IDENTIFIER_TYPE_HD_STATION_ID_EXT = 3; // 0x3 + field public static final int IDENTIFIER_TYPE_HD_SUBCHANNEL = 4; // 0x4 + field public static final int IDENTIFIER_TYPE_RDS_PI = 2; // 0x2 + field public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 12; // 0xc + field public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 11; // 0xb + field public static final int IDENTIFIER_TYPE_VENDOR1_PRIMARY = 13; // 0xd + field public static final int IDENTIFIER_TYPE_VENDOR2_PRIMARY = 14; // 0xe + field public static final int IDENTIFIER_TYPE_VENDOR3_PRIMARY = 15; // 0xf + field public static final int IDENTIFIER_TYPE_VENDOR4_PRIMARY = 16; // 0x10 + field public static final int PROGRAM_TYPE_AM = 1; // 0x1 + field public static final int PROGRAM_TYPE_AM_HD = 3; // 0x3 + field public static final int PROGRAM_TYPE_DAB = 5; // 0x5 + field public static final int PROGRAM_TYPE_DRMO = 6; // 0x6 + field public static final int PROGRAM_TYPE_FM = 2; // 0x2 + field public static final int PROGRAM_TYPE_FM_HD = 4; // 0x4 + field public static final int PROGRAM_TYPE_SXM = 7; // 0x7 + field public static final int PROGRAM_TYPE_VENDOR1 = 8; // 0x8 + field public static final int PROGRAM_TYPE_VENDOR2 = 9; // 0x9 + field public static final int PROGRAM_TYPE_VENDOR3 = 10; // 0xa + field public static final int PROGRAM_TYPE_VENDOR4 = 11; // 0xb + } + + public static final class ProgramSelector.Identifier implements android.os.Parcelable { + ctor public ProgramSelector.Identifier(int, long); + method public int describeContents(); + method public int getType(); + method public long getValue(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static abstract class ProgramSelector.IdentifierType implements java.lang.annotation.Annotation { + } + + public static abstract class ProgramSelector.ProgramType implements java.lang.annotation.Annotation { + } + public class RadioManager { method public int listModules(java.util.List); method public android.hardware.radio.RadioTuner openTuner(int, android.hardware.radio.RadioManager.BandConfig, boolean, android.hardware.radio.RadioTuner.Callback, android.os.Handler); @@ -17222,6 +17279,7 @@ package android.hardware.radio { field public static final int BAND_AM_HD = 3; // 0x3 field public static final int BAND_FM = 1; // 0x1 field public static final int BAND_FM_HD = 2; // 0x2 + field public static final int BAND_INVALID = -1; // 0xffffffff field public static final int CLASS_AM_FM = 0; // 0x0 field public static final int CLASS_DT = 2; // 0x2 field public static final int CLASS_SAT = 1; // 0x1 @@ -17257,6 +17315,9 @@ package android.hardware.radio { field public static final android.os.Parcelable.Creator CREATOR; } + public static abstract class RadioManager.Band implements java.lang.annotation.Annotation { + } + public static class RadioManager.BandConfig implements android.os.Parcelable { method public int describeContents(); method public int getLowerLimit(); @@ -17331,10 +17392,11 @@ package android.hardware.radio { public static class RadioManager.ProgramInfo implements android.os.Parcelable { method public int describeContents(); - method public int getChannel(); + method public deprecated int getChannel(); method public android.hardware.radio.RadioMetadata getMetadata(); + method public android.hardware.radio.ProgramSelector getSelector(); method public int getSignalStrength(); - method public int getSubChannel(); + method public deprecated int getSubChannel(); method public java.lang.String getVendorExension(); method public boolean isDigital(); method public boolean isLive(); @@ -17406,7 +17468,8 @@ package android.hardware.radio { method public abstract int setMute(boolean); method public abstract boolean startBackgroundScan(); method public abstract int step(int, boolean); - method public abstract int tune(int, int); + method public abstract deprecated int tune(int, int); + method public abstract void tune(android.hardware.radio.ProgramSelector); field public static final int DIRECTION_DOWN = 1; // 0x1 field public static final int DIRECTION_UP = 0; // 0x0 field public static final int ERROR_BACKGROUND_SCAN_FAILED = 6; // 0x6 diff --git a/core/java/android/hardware/radio/ITuner.aidl b/core/java/android/hardware/radio/ITuner.aidl index 69e135d448e75..2932c3ef80363 100644 --- a/core/java/android/hardware/radio/ITuner.aidl +++ b/core/java/android/hardware/radio/ITuner.aidl @@ -16,6 +16,7 @@ package android.hardware.radio; +import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; /** {@hide} */ @@ -52,7 +53,7 @@ interface ITuner { * @throws IllegalArgumentException if invalid arguments are passed * @throws IllegalStateException if called out of sequence */ - void tune(int channel, int subChannel); + void tune(in ProgramSelector selector); /** * @throws IllegalStateException if called out of sequence diff --git a/core/java/android/hardware/radio/ProgramSelector.aidl b/core/java/android/hardware/radio/ProgramSelector.aidl new file mode 100644 index 0000000000000..545269a20aa82 --- /dev/null +++ b/core/java/android/hardware/radio/ProgramSelector.aidl @@ -0,0 +1,20 @@ +/** + * 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; + +/** @hide */ +parcelable ProgramSelector; diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java new file mode 100644 index 0000000000000..4638dd5dc9501 --- /dev/null +++ b/core/java/android/hardware/radio/ProgramSelector.java @@ -0,0 +1,506 @@ +/** + * 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.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_VENDOR1 = 8; + public static final int PROGRAM_TYPE_VENDOR2 = 9; + public static final int PROGRAM_TYPE_VENDOR3 = 10; + public static final int PROGRAM_TYPE_VENDOR4 = 11; + @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, + PROGRAM_TYPE_VENDOR1, + PROGRAM_TYPE_VENDOR2, + PROGRAM_TYPE_VENDOR3, + PROGRAM_TYPE_VENDOR4, + }) + @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; + /** 32bit */ + public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 11; + /** 0-999 range */ + public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 12; + /** + * 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 VENDORx. + */ + public static final int IDENTIFIER_TYPE_VENDOR1_PRIMARY = 13; + public static final int IDENTIFIER_TYPE_VENDOR2_PRIMARY = 14; + public static final int IDENTIFIER_TYPE_VENDOR3_PRIMARY = 15; + public static final int IDENTIFIER_TYPE_VENDOR4_PRIMARY = 16; + @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_SXM_SERVICE_ID, + IDENTIFIER_TYPE_SXM_CHANNEL, + IDENTIFIER_TYPE_VENDOR1_PRIMARY, + IDENTIFIER_TYPE_VENDOR2_PRIMARY, + IDENTIFIER_TYPE_VENDOR3_PRIMARY, + IDENTIFIER_TYPE_VENDOR4_PRIMARY, + }) + @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. + * + * @returns program type. + */ + public @ProgramType int getProgramType() { + return mProgramType; + } + + /** + * Primary program identifier uniquely identifies a station and is used to + * determine equality between two ProgramSelectors. + * + * @returns primary identifier. + */ + public @NonNull Identifier getPrimaryId() { + return mPrimaryId; + } + + /** + * Secondary program identifier is not required for tuning, but may make it + * faster or more reliable. + * + * @returns 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 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 CREATOR = + new Parcelable.Creator() { + 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 CREATOR = + new Parcelable.Creator() { + public Identifier createFromParcel(Parcel in) { + return new Identifier(in); + } + + public Identifier[] newArray(int size) { + return new Identifier[size]; + } + }; + } +} diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java index d25e752f87736..8ef5705d67b15 100644 --- a/core/java/android/hardware/radio/RadioManager.java +++ b/core/java/android/hardware/radio/RadioManager.java @@ -16,6 +16,7 @@ package android.hardware.radio; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.SystemApi; import android.annotation.SystemService; @@ -29,6 +30,8 @@ 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.List; @@ -70,7 +73,7 @@ public class RadioManager { /** Radio module class supporting Digital terrestrial radio */ public static final int CLASS_DT = 2; - // keep in sync with radio_band_t in /system/core/incluse/system/radio.h + public static final int BAND_INVALID = -1; /** AM radio band (LW/MW/SW). * @see BandDescriptor */ public static final int BAND_AM = 0; @@ -83,6 +86,15 @@ public class RadioManager { /** 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. @@ -1258,8 +1270,7 @@ public class RadioManager { private final static int FLAG_LIVE = 1 << 0; private final static int FLAG_MUTED = 1 << 1; - private final int mChannel; - private final int mSubChannel; + @NonNull private final ProgramSelector mSelector; private final boolean mTuned; private final boolean mStereo; private final boolean mDigital; @@ -1268,11 +1279,10 @@ public class RadioManager { private final RadioMetadata mMetadata; private final String mVendorExension; - ProgramInfo(int channel, int subChannel, boolean tuned, boolean stereo, + ProgramInfo(@NonNull ProgramSelector selector, boolean tuned, boolean stereo, boolean digital, int signalStrength, RadioMetadata metadata, int flags, String vendorExension) { - mChannel = channel; - mSubChannel = subChannel; + mSelector = selector; mTuned = tuned; mStereo = stereo; mDigital = digital; @@ -1282,19 +1292,45 @@ public class RadioManager { mVendorExension = vendorExension; } + /** + * 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() { - return mChannel; + 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() { - return mSubChannel; + 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. */ @@ -1362,8 +1398,7 @@ public class RadioManager { } private ProgramInfo(Parcel in) { - mChannel = in.readInt(); - mSubChannel = in.readInt(); + mSelector = in.readParcelable(null); mTuned = in.readByte() == 1; mStereo = in.readByte() == 1; mDigital = in.readByte() == 1; @@ -1390,8 +1425,7 @@ public class RadioManager { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mChannel); - dest.writeInt(mSubChannel); + dest.writeParcelable(mSelector, 0); dest.writeByte((byte)(mTuned ? 1 : 0)); dest.writeByte((byte)(mStereo ? 1 : 0)); dest.writeByte((byte)(mDigital ? 1 : 0)); @@ -1413,7 +1447,7 @@ public class RadioManager { @Override public String toString() { - return "ProgramInfo [mChannel=" + mChannel + ", mSubChannel=" + mSubChannel + return "ProgramInfo [mSelector=" + mSelector + ", mTuned=" + mTuned + ", mStereo=" + mStereo + ", mDigital=" + mDigital + ", mFlags=" + mFlags + ", mSignalStrength=" + mSignalStrength + ((mMetadata == null) ? "" : (", mMetadata=" + mMetadata.toString())) @@ -1424,8 +1458,7 @@ public class RadioManager { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + mChannel; - result = prime * result + mSubChannel; + result = prime * result + mSelector.hashCode(); result = prime * result + (mTuned ? 1 : 0); result = prime * result + (mStereo ? 1 : 0); result = prime * result + (mDigital ? 1 : 0); @@ -1443,10 +1476,7 @@ public class RadioManager { if (!(obj instanceof ProgramInfo)) return false; ProgramInfo other = (ProgramInfo) obj; - if (mChannel != other.getChannel()) - return false; - if (mSubChannel != other.getSubChannel()) - return false; + if (!mSelector.equals(other.getSelector())) return false; if (mTuned != other.isTuned()) return false; if (mStereo != other.isStereo()) @@ -1541,7 +1571,7 @@ public class RadioManager { Log.e(TAG, "Failed to open tuner"); return null; } - return new TunerAdapter(tuner); + return new TunerAdapter(tuner, config != null ? config.getType() : BAND_INVALID); } @NonNull private final Context mContext; diff --git a/core/java/android/hardware/radio/RadioTuner.java b/core/java/android/hardware/radio/RadioTuner.java index 61e62eaabf8c7..f6592737c0711 100644 --- a/core/java/android/hardware/radio/RadioTuner.java +++ b/core/java/android/hardware/radio/RadioTuner.java @@ -170,9 +170,21 @@ public abstract class RadioTuner { *
  • {@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native * service fails,
  • * + * @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. + * + * @thows 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 diff --git a/core/java/android/hardware/radio/TunerAdapter.java b/core/java/android/hardware/radio/TunerAdapter.java index dbaca62e639f6..503bb55d6772c 100644 --- a/core/java/android/hardware/radio/TunerAdapter.java +++ b/core/java/android/hardware/radio/TunerAdapter.java @@ -32,11 +32,14 @@ class TunerAdapter extends RadioTuner { @NonNull private final ITuner mTuner; private boolean mIsClosed = false; - TunerAdapter(ITuner tuner) { + private @RadioManager.Band int mBand; + + TunerAdapter(ITuner tuner, @RadioManager.Band int band) { if (tuner == null) { throw new NullPointerException(); } mTuner = tuner; + mBand = band; } @Override @@ -59,6 +62,7 @@ class TunerAdapter extends RadioTuner { 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); @@ -138,7 +142,7 @@ class TunerAdapter extends RadioTuner { @Override public int tune(int channel, int subChannel) { try { - mTuner.tune(channel, subChannel); + mTuner.tune(ProgramSelector.createAmFmSelector(mBand, channel, subChannel)); } catch (IllegalStateException e) { Log.e(TAG, "Can't tune", e); return RadioManager.STATUS_INVALID_OPERATION; @@ -152,6 +156,15 @@ class TunerAdapter extends RadioTuner { 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 { diff --git a/services/core/java/com/android/server/radio/Tuner.java b/services/core/java/com/android/server/radio/Tuner.java index 81128c22fb0f9..209febc204f10 100644 --- a/services/core/java/com/android/server/radio/Tuner.java +++ b/services/core/java/com/android/server/radio/Tuner.java @@ -19,6 +19,7 @@ package com.android.server.radio; import android.annotation.NonNull; import android.hardware.radio.ITuner; import android.hardware.radio.ITunerCallback; +import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; import android.os.IBinder; import android.os.RemoteException; @@ -44,12 +45,13 @@ class Tuner extends ITuner.Stub { private int mRegion; // TODO(b/62710330): find better solution to handle regions private final boolean mWithAudio; - Tuner(@NonNull ITunerCallback clientCallback, int halRev, int region, boolean withAudio) { + Tuner(@NonNull ITunerCallback clientCallback, int halRev, + int region, boolean withAudio, int band) { mClientCallback = clientCallback; mTunerCallback = new TunerCallback(this, clientCallback, halRev); mRegion = region; mWithAudio = withAudio; - mNativeContext = nativeInit(halRev, withAudio); + mNativeContext = nativeInit(halRev, withAudio, band); mDeathRecipient = this::close; try { mClientCallback.asBinder().linkToDeath(mDeathRecipient, 0); @@ -64,7 +66,7 @@ class Tuner extends ITuner.Stub { super.finalize(); } - private native long nativeInit(int halRev, boolean withAudio); + private native long nativeInit(int halRev, boolean withAudio, int band); private native void nativeFinalize(long nativeContext); private native void nativeClose(long nativeContext); @@ -74,7 +76,7 @@ class Tuner extends ITuner.Stub { private native void nativeStep(long nativeContext, boolean directionDown, boolean skipSubChannel); private native void nativeScan(long nativeContext, boolean directionDown, boolean skipSubChannel); - private native void nativeTune(long nativeContext, int channel, int subChannel); + private native void nativeTune(long nativeContext, @NonNull ProgramSelector selector); private native void nativeCancel(long nativeContext); private native RadioManager.ProgramInfo nativeGetProgramInformation(long nativeContext); @@ -173,10 +175,14 @@ class Tuner extends ITuner.Stub { } @Override - public void tune(int channel, int subChannel) { + public void tune(ProgramSelector selector) { + if (selector == null) { + throw new IllegalArgumentException("The argument must not be a null pointer"); + } + Slog.i(TAG, "Tuning to " + selector); synchronized (mLock) { checkNotClosedLocked(); - nativeTune(mNativeContext, channel, subChannel); + nativeTune(mNativeContext, selector); } } diff --git a/services/core/jni/Android.mk b/services/core/jni/Android.mk index c8c629fd5ea54..a60650251def4 100644 --- a/services/core/jni/Android.mk +++ b/services/core/jni/Android.mk @@ -108,4 +108,6 @@ LOCAL_SHARED_LIBRARIES += \ android.frameworks.schedulerservice@1.0 \ android.frameworks.sensorservice@1.0 \ -LOCAL_STATIC_LIBRARIES += libscrypt_static +LOCAL_STATIC_LIBRARIES += \ + android.hardware.broadcastradio@1.1-utils-lib \ + libscrypt_static \ diff --git a/services/core/jni/com_android_server_radio_RadioService.cpp b/services/core/jni/com_android_server_radio_RadioService.cpp index fa64901caee90..459c0d05ca773 100644 --- a/services/core/jni/com_android_server_radio_RadioService.cpp +++ b/services/core/jni/com_android_server_radio_RadioService.cpp @@ -210,7 +210,7 @@ static jobject nativeOpenTuner(JNIEnv *env, jobject obj, long nativeContext, jin BandConfig bandConfigHal = convert::BandConfigToHal(env, bandConfig, region); auto tuner = make_javaref(env, env->NewObject(gjni.Tuner.clazz, gjni.Tuner.cstor, - callback, halRev, region, withAudio)); + callback, halRev, region, withAudio, bandConfigHal.type)); if (tuner == nullptr) { ALOGE("Unable to create new tuner object."); return nullptr; @@ -258,7 +258,7 @@ void register_android_server_radio_RadioService(JNIEnv *env) { auto tunerClass = FindClassOrDie(env, "com/android/server/radio/Tuner"); gjni.Tuner.clazz = MakeGlobalRefOrDie(env, tunerClass); gjni.Tuner.cstor = GetMethodIDOrDie(env, tunerClass, "", - "(Landroid/hardware/radio/ITunerCallback;IIZ)V"); + "(Landroid/hardware/radio/ITunerCallback;IIZI)V"); auto arrayListClass = FindClassOrDie(env, "java/util/ArrayList"); gjni.ArrayList.clazz = MakeGlobalRefOrDie(env, arrayListClass); diff --git a/services/core/jni/com_android_server_radio_Tuner.cpp b/services/core/jni/com_android_server_radio_Tuner.cpp index 4b6f30b1c09f3..13bfb57201a48 100644 --- a/services/core/jni/com_android_server_radio_Tuner.cpp +++ b/services/core/jni/com_android_server_radio_Tuner.cpp @@ -22,12 +22,13 @@ #include "com_android_server_radio_convert.h" #include "com_android_server_radio_TunerCallback.h" +#include +#include #include #include #include #include #include -#include namespace android { namespace server { @@ -41,6 +42,7 @@ using hardware::hidl_vec; namespace V1_0 = hardware::broadcastradio::V1_0; namespace V1_1 = hardware::broadcastradio::V1_1; +using V1_0::Band; using V1_0::BandConfig; using V1_0::MetaData; using V1_0::Result; @@ -79,6 +81,7 @@ struct TunerContext { bool mIsClosed = false; HalRevision mHalRev; bool mWithAudio; + Band mBand; sp mHalTuner; sp mHalTuner11; sp mHalDeathRecipient; @@ -100,13 +103,14 @@ static TunerContext& getNativeContext(JNIEnv *env, JavaRef const &jTune return getNativeContext(env->GetLongField(jTuner.get(), gjni.Tuner.nativeContext)); } -static jlong nativeInit(JNIEnv *env, jobject obj, jint halRev, bool withAudio) { +static jlong nativeInit(JNIEnv *env, jobject obj, jint halRev, bool withAudio, jint band) { ALOGV("nativeInit()"); AutoMutex _l(gContextMutex); auto ctx = new TunerContext(); ctx->mHalRev = static_cast(halRev); ctx->mWithAudio = withAudio; + ctx->mBand = static_cast(band); static_assert(sizeof(jlong) >= sizeof(ctx), "jlong is smaller than a pointer"); return reinterpret_cast(ctx); @@ -170,13 +174,17 @@ void setHalTuner(JNIEnv *env, JavaRef const &jTuner, sp h notifyAudioService(ctx, true); } -sp getHalTuner(jlong nativeContext) { - AutoMutex _l(gContextMutex); - auto tuner = getNativeContext(nativeContext).mHalTuner; +static sp getHalTuner(const TunerContext& ctx) { + auto tuner = ctx.mHalTuner; LOG_ALWAYS_FATAL_IF(tuner == nullptr, "HAL tuner is not open"); return tuner; } +sp getHalTuner(jlong nativeContext) { + AutoMutex _l(gContextMutex); + return getHalTuner(getNativeContext(nativeContext)); +} + sp getHalTuner11(jlong nativeContext) { AutoMutex _l(gContextMutex); return getNativeContext(nativeContext).mHalTuner11; @@ -215,13 +223,17 @@ static void nativeClose(JNIEnv *env, jobject obj, jlong nativeContext) { static void nativeSetConfiguration(JNIEnv *env, jobject obj, jlong nativeContext, jobject config) { ALOGV("nativeSetConfiguration()"); - auto halTuner = getHalTuner(nativeContext); + AutoMutex _l(gContextMutex); + auto& ctx = getNativeContext(nativeContext); + auto halTuner = getHalTuner(ctx); if (halTuner == nullptr) return; Region region_unused; BandConfig bandConfigHal = convert::BandConfigToHal(env, config, region_unused); - convert::ThrowIfFailed(env, halTuner->setConfiguration(bandConfigHal)); + if (convert::ThrowIfFailed(env, halTuner->setConfiguration(bandConfigHal))) return; + + ctx.mBand = bandConfigHal.type; } static jobject nativeGetConfiguration(JNIEnv *env, jobject obj, jlong nativeContext, @@ -263,13 +275,26 @@ static void nativeScan(JNIEnv *env, jobject obj, jlong nativeContext, convert::ThrowIfFailed(env, halTuner->scan(dir, skipSubChannel)); } -static void nativeTune(JNIEnv *env, jobject obj, jlong nativeContext, - jint channel, jint subChannel) { - ALOGV("nativeTune(%d, %d)", channel, subChannel); - auto halTuner = getHalTuner(nativeContext); - if (halTuner == nullptr) return; +static void nativeTune(JNIEnv *env, jobject obj, jlong nativeContext, jobject jSelector) { + ALOGV("nativeTune()"); + AutoMutex _l(gContextMutex); + auto& ctx = getNativeContext(nativeContext); + auto halTuner10 = getHalTuner(ctx); + auto halTuner11 = ctx.mHalTuner11; + if (halTuner10 == nullptr) return; - convert::ThrowIfFailed(env, halTuner->tune(channel, subChannel)); + auto selector = convert::ProgramSelectorToHal(env, jSelector); + if (halTuner11 != nullptr) { + convert::ThrowIfFailed(env, halTuner11->tune_1_1(selector)); + } else { + uint32_t channel, subChannel; + if (!V1_1::utils::getLegacyChannel(selector, &channel, &subChannel)) { + jniThrowException(env, "java/lang/IllegalArgumentException", + "Can't tune to non-AM/FM channel with HAL<1.1"); + return; + } + convert::ThrowIfFailed(env, halTuner10->tune(channel, subChannel)); + } } static void nativeCancel(JNIEnv *env, jobject obj, jlong nativeContext) { @@ -282,8 +307,10 @@ static void nativeCancel(JNIEnv *env, jobject obj, jlong nativeContext) { static jobject nativeGetProgramInformation(JNIEnv *env, jobject obj, jlong nativeContext) { ALOGV("nativeGetProgramInformation()"); - auto halTuner10 = getHalTuner(nativeContext); - auto halTuner11 = getHalTuner11(nativeContext); + AutoMutex _l(gContextMutex); + auto& ctx = getNativeContext(nativeContext); + auto halTuner10 = getHalTuner(ctx); + auto halTuner11 = ctx.mHalTuner11; if (halTuner10 == nullptr) return nullptr; JavaRef jInfo; @@ -301,7 +328,7 @@ static jobject nativeGetProgramInformation(JNIEnv *env, jobject obj, jlong nativ const V1_0::ProgramInfo& info) { halResult = result; if (result != Result::OK) return; - jInfo = convert::ProgramInfoFromHal(env, info); + jInfo = convert::ProgramInfoFromHal(env, info, ctx.mBand); }); } @@ -402,7 +429,7 @@ static bool nativeIsAntennaConnected(JNIEnv *env, jobject obj, jlong nativeConte } static const JNINativeMethod gTunerMethods[] = { - { "nativeInit", "(IZ)J", (void*)nativeInit }, + { "nativeInit", "(IZI)J", (void*)nativeInit }, { "nativeFinalize", "(J)V", (void*)nativeFinalize }, { "nativeClose", "(J)V", (void*)nativeClose }, { "nativeSetConfiguration", "(JLandroid/hardware/radio/RadioManager$BandConfig;)V", @@ -411,7 +438,7 @@ static const JNINativeMethod gTunerMethods[] = { (void*)nativeGetConfiguration }, { "nativeStep", "(JZZ)V", (void*)nativeStep }, { "nativeScan", "(JZZ)V", (void*)nativeScan }, - { "nativeTune", "(JII)V", (void*)nativeTune }, + { "nativeTune", "(JLandroid/hardware/radio/ProgramSelector;)V", (void*)nativeTune }, { "nativeCancel", "(J)V", (void*)nativeCancel }, { "nativeGetProgramInformation", "(J)Landroid/hardware/radio/RadioManager$ProgramInfo;", (void*)nativeGetProgramInformation }, diff --git a/services/core/jni/com_android_server_radio_convert.cpp b/services/core/jni/com_android_server_radio_convert.cpp index 55fb10fb78023..af000bdbfc858 100644 --- a/services/core/jni/com_android_server_radio_convert.cpp +++ b/services/core/jni/com_android_server_radio_convert.cpp @@ -19,9 +19,10 @@ #include "com_android_server_radio_convert.h" +#include +#include #include #include -#include namespace android { namespace server { @@ -37,7 +38,9 @@ using V1_0::Direction; using V1_0::MetadataType; using V1_0::Result; using V1_0::Rds; +using V1_1::ProgramIdentifier; using V1_1::ProgramListResult; +using V1_1::ProgramSelector; static JavaRef BandDescriptorFromHal(JNIEnv *env, const V1_0::BandConfig &config, Region region); @@ -87,6 +90,22 @@ static struct { jmethodID cstor; } ProgramInfo; + struct { + jclass clazz; + jmethodID cstor; + jfieldID programType; + jfieldID primaryId; + jfieldID secondaryIds; + jfieldID vendorIds; + + struct { + jclass clazz; + jmethodID cstor; + jfieldID type; + jfieldID value; + } Identifier; + } ProgramSelector; + struct { jclass clazz; jmethodID cstor; @@ -383,25 +402,100 @@ JavaRef MetadataFromHal(JNIEnv *env, const hidl_vec &me return jMetadata; } +static JavaRef ProgramIdentifierFromHal(JNIEnv *env, const ProgramIdentifier &id) { + ALOGV("ProgramIdentifierFromHal()"); + return make_javaref(env, env->NewObject(gjni.ProgramSelector.Identifier.clazz, + gjni.ProgramSelector.Identifier.cstor, id.type, id.value)); +} + +static JavaRef ProgramSelectorFromHal(JNIEnv *env, const ProgramSelector &selector) { + ALOGV("ProgramSelectorFromHal()"); + auto jPrimary = ProgramIdentifierFromHal(env, selector.primaryId); + + auto jSecondary = make_javaref(env, env->NewObjectArray(selector.secondaryIds.size(), + gjni.ProgramSelector.Identifier.clazz, nullptr)); + for (size_t i = 0; i < selector.secondaryIds.size(); i++) { + auto jId = ProgramIdentifierFromHal(env, selector.secondaryIds[i]); + env->SetObjectArrayElement(jSecondary.get(), i, jId.get()); + } + + auto jVendor = make_javaref(env, env->NewLongArray(selector.vendorIds.size())); + auto jVendorElements = env->GetLongArrayElements(jVendor.get(), nullptr); + for (size_t i = 0; i < selector.vendorIds.size(); i++) { + jVendorElements[i] = selector.vendorIds[i]; + } + env->ReleaseLongArrayElements(jVendor.get(), jVendorElements, 0); + + return make_javaref(env, env->NewObject(gjni.ProgramSelector.clazz, gjni.ProgramSelector.cstor, + selector.programType, jPrimary.get(), jSecondary.get(), jVendor.get())); +} + +static ProgramIdentifier ProgramIdentifierToHal(JNIEnv *env, jobject jId) { + ALOGV("ProgramIdentifierToHal()"); + + ProgramIdentifier id = {}; + id.type = env->GetIntField(jId, gjni.ProgramSelector.Identifier.type); + id.value = env->GetLongField(jId, gjni.ProgramSelector.Identifier.value); + return id; +} + +ProgramSelector ProgramSelectorToHal(JNIEnv *env, jobject jSelector) { + ALOGV("ProgramSelectorToHal()"); + + ProgramSelector selector = {}; + + selector.programType = env->GetIntField(jSelector, gjni.ProgramSelector.programType); + + auto jPrimary = env->GetObjectField(jSelector, gjni.ProgramSelector.primaryId); + auto jSecondary = reinterpret_cast( + env->GetObjectField(jSelector, gjni.ProgramSelector.secondaryIds)); + auto jVendor = reinterpret_cast( + env->GetObjectField(jSelector, gjni.ProgramSelector.vendorIds)); + + if (jPrimary == nullptr || jSecondary == nullptr || jVendor == nullptr) { + ALOGE("ProgramSelector object is incomplete"); + return {}; + } + + selector.primaryId = ProgramIdentifierToHal(env, jPrimary); + auto count = env->GetArrayLength(jSecondary); + selector.secondaryIds.resize(count); + for (jsize i = 0; i < count; i++) { + auto jId = env->GetObjectArrayElement(jSecondary, i); + selector.secondaryIds[i] = ProgramIdentifierToHal(env, jId); + } + + count = env->GetArrayLength(jVendor); + selector.vendorIds.resize(count); + auto jVendorElements = env->GetLongArrayElements(jVendor, nullptr); + for (jint i = 0; i < count; i++) { + selector.vendorIds[i] = jVendorElements[i]; + } + env->ReleaseLongArrayElements(jVendor, jVendorElements, 0); + + return selector; +} + static JavaRef ProgramInfoFromHal(JNIEnv *env, const V1_0::ProgramInfo &info10, - const V1_1::ProgramInfo *info11) { + const V1_1::ProgramInfo *info11, const ProgramSelector &selector) { ALOGV("ProgramInfoFromHal()"); auto jMetadata = MetadataFromHal(env, info10.metadata); auto jVendorExtension = info11 ? make_javastr(env, info11->vendorExension) : nullptr; + auto jSelector = ProgramSelectorFromHal(env, selector); return make_javaref(env, env->NewObject(gjni.ProgramInfo.clazz, gjni.ProgramInfo.cstor, - info10.channel, info10.subChannel, info10.tuned, info10.stereo, info10.digital, - info10.signalStrength, jMetadata.get(), info11 ? info11->flags : 0, - jVendorExtension.get())); + jSelector.get(), info10.tuned, info10.stereo, info10.digital, info10.signalStrength, + jMetadata.get(), info11 ? info11->flags : 0, jVendorExtension.get())); } -JavaRef ProgramInfoFromHal(JNIEnv *env, const V1_0::ProgramInfo &info) { - return ProgramInfoFromHal(env, info, nullptr); +JavaRef ProgramInfoFromHal(JNIEnv *env, const V1_0::ProgramInfo &info, V1_0::Band band) { + auto selector = V1_1::utils::make_selector(band, info.channel, info.subChannel); + return ProgramInfoFromHal(env, info, nullptr, selector); } JavaRef ProgramInfoFromHal(JNIEnv *env, const V1_1::ProgramInfo &info) { - return ProgramInfoFromHal(env, info.base, &info); + return ProgramInfoFromHal(env, info.base, &info, info.selector); } } // namespace convert @@ -465,7 +559,31 @@ void register_android_server_radio_convert(JNIEnv *env) { auto programInfoClass = FindClassOrDie(env, "android/hardware/radio/RadioManager$ProgramInfo"); gjni.ProgramInfo.clazz = MakeGlobalRefOrDie(env, programInfoClass); gjni.ProgramInfo.cstor = GetMethodIDOrDie(env, programInfoClass, "", - "(IIZZZILandroid/hardware/radio/RadioMetadata;ILjava/lang/String;)V"); + "(Landroid/hardware/radio/ProgramSelector;ZZZILandroid/hardware/radio/RadioMetadata;I" + "Ljava/lang/String;)V"); + + auto programSelectorClass = FindClassOrDie(env, "android/hardware/radio/ProgramSelector"); + gjni.ProgramSelector.clazz = MakeGlobalRefOrDie(env, programSelectorClass); + gjni.ProgramSelector.cstor = GetMethodIDOrDie(env, programSelectorClass, "", + "(ILandroid/hardware/radio/ProgramSelector$Identifier;" + "[Landroid/hardware/radio/ProgramSelector$Identifier;[J)V"); + gjni.ProgramSelector.programType = GetFieldIDOrDie(env, programSelectorClass, + "mProgramType", "I"); + gjni.ProgramSelector.primaryId = GetFieldIDOrDie(env, programSelectorClass, + "mPrimaryId", "Landroid/hardware/radio/ProgramSelector$Identifier;"); + gjni.ProgramSelector.secondaryIds = GetFieldIDOrDie(env, programSelectorClass, + "mSecondaryIds", "[Landroid/hardware/radio/ProgramSelector$Identifier;"); + gjni.ProgramSelector.vendorIds = GetFieldIDOrDie(env, programSelectorClass, + "mVendorIds", "[J"); + + auto progSelIdClass = FindClassOrDie(env, "android/hardware/radio/ProgramSelector$Identifier"); + gjni.ProgramSelector.Identifier.clazz = MakeGlobalRefOrDie(env, progSelIdClass); + gjni.ProgramSelector.Identifier.cstor = GetMethodIDOrDie(env, progSelIdClass, + "", "(IJ)V"); + gjni.ProgramSelector.Identifier.type = GetFieldIDOrDie(env, progSelIdClass, + "mType", "I"); + gjni.ProgramSelector.Identifier.value = GetFieldIDOrDie(env, progSelIdClass, + "mValue", "J"); auto radioMetadataClass = FindClassOrDie(env, "android/hardware/radio/RadioMetadata"); gjni.RadioMetadata.clazz = MakeGlobalRefOrDie(env, radioMetadataClass); diff --git a/services/core/jni/com_android_server_radio_convert.h b/services/core/jni/com_android_server_radio_convert.h index 3ba6fed2959b5..8b91ced923321 100644 --- a/services/core/jni/com_android_server_radio_convert.h +++ b/services/core/jni/com_android_server_radio_convert.h @@ -45,9 +45,10 @@ V1_0::BandConfig BandConfigToHal(JNIEnv *env, jobject jConfig, Region ®ion); V1_0::Direction DirectionToHal(bool directionDown); JavaRef MetadataFromHal(JNIEnv *env, const hardware::hidl_vec &metadata); -JavaRef ProgramInfoFromHal(JNIEnv *env, const V1_0::ProgramInfo &info); +JavaRef ProgramInfoFromHal(JNIEnv *env, const V1_0::ProgramInfo &info, V1_0::Band band); JavaRef ProgramInfoFromHal(JNIEnv *env, const V1_1::ProgramInfo &info); +V1_1::ProgramSelector ProgramSelectorToHal(JNIEnv *env, jobject jSelector); void ThrowParcelableRuntimeException(JNIEnv *env, const std::string& msg); diff --git a/tests/radio/src/android/hardware/radio/tests/RadioTest.java b/tests/radio/src/android/hardware/radio/tests/RadioTest.java index e354a34942bbc..0c6d0e237e2c5 100644 --- a/tests/radio/src/android/hardware/radio/tests/RadioTest.java +++ b/tests/radio/src/android/hardware/radio/tests/RadioTest.java @@ -18,6 +18,7 @@ package android.hardware.radio.tests; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; +import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; import android.hardware.radio.RadioTuner; import android.support.test.InstrumentationRegistry; @@ -64,6 +65,7 @@ public class RadioTest { private final int kConfigCallbackTimeoutMs = 10000; private final int kCancelTimeoutMs = 1000; private final int kTuneCallbackTimeoutMs = 30000; + private final int kFullScanTimeoutMs = 60000; private RadioManager mRadioManager; private RadioTuner mRadioTuner; @@ -144,11 +146,9 @@ public class RadioTest { assertNotNull(mRadioTuner); verify(mCallback, timeout(kConfigCallbackTimeoutMs)).onConfigurationChanged(any()); resetCallback(); - } - private void checkAntenna() { - boolean isConnected = mRadioTuner.isAntennaConnected(); - assertTrue(isConnected); + boolean isAntennaConnected = mRadioTuner.isAntennaConnected(); + assertTrue(isAntennaConnected); } @Test @@ -256,7 +256,6 @@ public class RadioTest { @Test public void testStep() { openTuner(); - checkAntenna(); int ret = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, true); assertEquals(RadioManager.STATUS_OK, ret); @@ -272,7 +271,6 @@ public class RadioTest { @Test public void testTuneAndGetPI() { openTuner(); - checkAntenna(); int channel = mFmBandConfig.getLowerLimit() + mFmBandConfig.getSpacing(); @@ -304,7 +302,6 @@ public class RadioTest { @Test public void testLateCancel() { openTuner(); - checkAntenna(); int ret = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, false); assertEquals(RadioManager.STATUS_OK, ret); @@ -317,7 +314,6 @@ public class RadioTest { @Test public void testScanAndCancel() { openTuner(); - checkAntenna(); /* There is a possible race condition between scan and cancel commands - the scan may finish * before cancel command is issued. Thus we accept both outcomes in this test. @@ -335,7 +331,6 @@ public class RadioTest { @Test public void testStartBackgroundScan() { openTuner(); - checkAntenna(); boolean ret = mRadioTuner.startBackgroundScan(); boolean isSupported = mModule.isBackgroundScanningSupported(); @@ -345,7 +340,6 @@ public class RadioTest { @Test public void testGetProgramList() { openTuner(); - checkAntenna(); try { List list = mRadioTuner.getProgramList(null); @@ -356,6 +350,39 @@ public class RadioTest { } } + @Test + public void testTuneFromProgramList() { + openTuner(); + + List list; + + try { + list = mRadioTuner.getProgramList(null); + assertNotNull(list); + } catch (IllegalStateException e) { + Log.i(TAG, "Background list is not ready, trying to fix it"); + + boolean success = mRadioTuner.startBackgroundScan(); + assertTrue(success); + verify(mCallback, timeout(kFullScanTimeoutMs)).onBackgroundScanComplete(); + + list = mRadioTuner.getProgramList(null); + assertNotNull(list); + } + + if (list.isEmpty()) { + Log.i(TAG, "Program list is empty, can't test tune"); + return; + } + + ProgramSelector sel = list.get(0).getSelector(); + mRadioTuner.tune(sel); + ArgumentCaptor infoc = + ArgumentCaptor.forClass(RadioManager.ProgramInfo.class); + verify(mCallback, timeout(kTuneCallbackTimeoutMs)).onProgramInfoChanged(infoc.capture()); + assertEquals(sel, infoc.getValue().getSelector()); + } + @Test public void testForcedAnalog() { openTuner();