diff options
author | Jeff Davidson <jpd@google.com> | 2018-02-08 15:30:06 -0800 |
---|---|---|
committer | Jeff Davidson <jpd@google.com> | 2018-02-08 15:30:06 -0800 |
commit | a192cc2a132cb0ee8588e2df755563ec7008c179 (patch) | |
tree | 380e4db22df19c819bd37df34bf06e7568916a50 /android/hardware/radio | |
parent | 98fe7819c6d14f4f464a5cac047f9e82dee5da58 (diff) | |
download | android-28-a192cc2a132cb0ee8588e2df755563ec7008c179.tar.gz |
Update fullsdk to 4575844
/google/data/ro/projects/android/fetch_artifact \
--bid 4575844 \
--target sdk_phone_x86_64-sdk \
sdk-repo-linux-sources-4575844.zip
Test: TreeHugger
Change-Id: I81e0eb157b4ac3b38408d0ef86f9d6286471f87a
Diffstat (limited to 'android/hardware/radio')
-rw-r--r-- | android/hardware/radio/Announcement.java | 133 | ||||
-rw-r--r-- | android/hardware/radio/ProgramList.java | 427 | ||||
-rw-r--r-- | android/hardware/radio/ProgramSelector.java | 164 | ||||
-rw-r--r-- | android/hardware/radio/RadioManager.java | 401 | ||||
-rw-r--r-- | android/hardware/radio/RadioTuner.java | 76 | ||||
-rw-r--r-- | android/hardware/radio/TunerAdapter.java | 92 | ||||
-rw-r--r-- | android/hardware/radio/TunerCallbackAdapter.java | 73 | ||||
-rw-r--r-- | android/hardware/radio/Utils.java | 118 |
8 files changed, 1332 insertions, 152 deletions
diff --git a/android/hardware/radio/Announcement.java b/android/hardware/radio/Announcement.java new file mode 100644 index 00000000..166fe604 --- /dev/null +++ b/android/hardware/radio/Announcement.java @@ -0,0 +1,133 @@ +/** + * Copyright (C) 2018 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.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @hide + */ +@SystemApi +public final class Announcement implements Parcelable { + + /** DAB alarm, RDS emergency program type (PTY 31). */ + public static final int TYPE_EMERGENCY = 1; + /** DAB warning. */ + public static final int TYPE_WARNING = 2; + /** DAB road traffic, RDS TA, HD Radio transportation. */ + public static final int TYPE_TRAFFIC = 3; + /** Weather. */ + public static final int TYPE_WEATHER = 4; + /** News. */ + public static final int TYPE_NEWS = 5; + /** DAB event, special event. */ + public static final int TYPE_EVENT = 6; + /** DAB sport report, RDS sports. */ + public static final int TYPE_SPORT = 7; + /** All others. */ + public static final int TYPE_MISC = 8; + /** @hide */ + @IntDef(prefix = { "TYPE_" }, value = { + TYPE_EMERGENCY, + TYPE_WARNING, + TYPE_TRAFFIC, + TYPE_WEATHER, + TYPE_NEWS, + TYPE_EVENT, + TYPE_SPORT, + TYPE_MISC, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Type {} + + /** + * Listener of announcement list events. + */ + public interface OnListUpdatedListener { + /** + * An event called whenever a list of active announcements change. + * + * The entire list is sent each time a new announcement appears or any ends broadcasting. + * + * @param activeAnnouncements a full list of active announcements + */ + void onListUpdated(Collection<Announcement> activeAnnouncements); + } + + @NonNull private final ProgramSelector mSelector; + @Type private final int mType; + @NonNull private final Map<String, String> mVendorInfo; + + /** @hide */ + public Announcement(@NonNull ProgramSelector selector, @Type int type, + @NonNull Map<String, String> vendorInfo) { + mSelector = Objects.requireNonNull(selector); + mType = Objects.requireNonNull(type); + mVendorInfo = Objects.requireNonNull(vendorInfo); + } + + private Announcement(@NonNull Parcel in) { + mSelector = in.readTypedObject(ProgramSelector.CREATOR); + mType = in.readInt(); + mVendorInfo = Utils.readStringMap(in); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedObject(mSelector, 0); + dest.writeInt(mType); + Utils.writeStringMap(dest, mVendorInfo); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<Announcement> CREATOR = + new Parcelable.Creator<Announcement>() { + public Announcement createFromParcel(Parcel in) { + return new Announcement(in); + } + + public Announcement[] newArray(int size) { + return new Announcement[size]; + } + }; + + public @NonNull ProgramSelector getSelector() { + return mSelector; + } + + public @Type int getType() { + return mType; + } + + public @NonNull Map<String, String> getVendorInfo() { + return mVendorInfo; + } +} diff --git a/android/hardware/radio/ProgramList.java b/android/hardware/radio/ProgramList.java new file mode 100644 index 00000000..b2aa9ba5 --- /dev/null +++ b/android/hardware/radio/ProgramList.java @@ -0,0 +1,427 @@ +/** + * Copyright (C) 2018 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.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; + +/** + * @hide + */ +@SystemApi +public final class ProgramList implements AutoCloseable { + + private final Object mLock = new Object(); + private final Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> mPrograms = + new HashMap<>(); + + private final List<ListCallback> mListCallbacks = new ArrayList<>(); + private final List<OnCompleteListener> mOnCompleteListeners = new ArrayList<>(); + private OnCloseListener mOnCloseListener; + private boolean mIsClosed = false; + private boolean mIsComplete = false; + + ProgramList() {} + + /** + * Callback for list change operations. + */ + public abstract static class ListCallback { + /** + * Called when item was modified or added to the list. + */ + public void onItemChanged(@NonNull ProgramSelector.Identifier id) { } + + /** + * Called when item was removed from the list. + */ + public void onItemRemoved(@NonNull ProgramSelector.Identifier id) { } + } + + /** + * Listener of list complete event. + */ + public interface OnCompleteListener { + /** + * Called when the list turned complete (i.e. when the scan process + * came to an end). + */ + void onComplete(); + } + + interface OnCloseListener { + void onClose(); + } + + /** + * Registers list change callback with executor. + */ + public void registerListCallback(@NonNull @CallbackExecutor Executor executor, + @NonNull ListCallback callback) { + registerListCallback(new ListCallback() { + public void onItemChanged(@NonNull ProgramSelector.Identifier id) { + executor.execute(() -> callback.onItemChanged(id)); + } + + public void onItemRemoved(@NonNull ProgramSelector.Identifier id) { + executor.execute(() -> callback.onItemRemoved(id)); + } + }); + } + + /** + * Registers list change callback. + */ + public void registerListCallback(@NonNull ListCallback callback) { + synchronized (mLock) { + if (mIsClosed) return; + mListCallbacks.add(Objects.requireNonNull(callback)); + } + } + + /** + * Unregisters list change callback. + */ + public void unregisterListCallback(@NonNull ListCallback callback) { + synchronized (mLock) { + if (mIsClosed) return; + mListCallbacks.remove(Objects.requireNonNull(callback)); + } + } + + /** + * Adds list complete event listener with executor. + */ + public void addOnCompleteListener(@NonNull @CallbackExecutor Executor executor, + @NonNull OnCompleteListener listener) { + addOnCompleteListener(() -> executor.execute(listener::onComplete)); + } + + /** + * Adds list complete event listener. + */ + public void addOnCompleteListener(@NonNull OnCompleteListener listener) { + synchronized (mLock) { + if (mIsClosed) return; + mOnCompleteListeners.add(Objects.requireNonNull(listener)); + if (mIsComplete) listener.onComplete(); + } + } + + /** + * Removes list complete event listener. + */ + public void removeOnCompleteListener(@NonNull OnCompleteListener listener) { + synchronized (mLock) { + if (mIsClosed) return; + mOnCompleteListeners.remove(Objects.requireNonNull(listener)); + } + } + + void setOnCloseListener(@Nullable OnCloseListener listener) { + synchronized (mLock) { + if (mOnCloseListener != null) { + throw new IllegalStateException("Close callback is already set"); + } + mOnCloseListener = listener; + } + } + + /** + * Disables list updates and releases all resources. + */ + public void close() { + synchronized (mLock) { + if (mIsClosed) return; + mIsClosed = true; + mPrograms.clear(); + mListCallbacks.clear(); + mOnCompleteListeners.clear(); + if (mOnCloseListener != null) { + mOnCloseListener.onClose(); + mOnCloseListener = null; + } + } + } + + void apply(@NonNull Chunk chunk) { + synchronized (mLock) { + if (mIsClosed) return; + + mIsComplete = false; + + if (chunk.isPurge()) { + new HashSet<>(mPrograms.keySet()).stream().forEach(id -> removeLocked(id)); + } + + chunk.getRemoved().stream().forEach(id -> removeLocked(id)); + chunk.getModified().stream().forEach(info -> putLocked(info)); + + if (chunk.isComplete()) { + mIsComplete = true; + mOnCompleteListeners.forEach(cb -> cb.onComplete()); + } + } + } + + private void putLocked(@NonNull RadioManager.ProgramInfo value) { + ProgramSelector.Identifier key = value.getSelector().getPrimaryId(); + mPrograms.put(Objects.requireNonNull(key), value); + ProgramSelector.Identifier sel = value.getSelector().getPrimaryId(); + mListCallbacks.forEach(cb -> cb.onItemChanged(sel)); + } + + private void removeLocked(@NonNull ProgramSelector.Identifier key) { + RadioManager.ProgramInfo removed = mPrograms.remove(Objects.requireNonNull(key)); + if (removed == null) return; + ProgramSelector.Identifier sel = removed.getSelector().getPrimaryId(); + mListCallbacks.forEach(cb -> cb.onItemRemoved(sel)); + } + + /** + * Converts the program list in its current shape to the static List<>. + * + * @return the new List<> object; it won't receive any further updates + */ + public @NonNull List<RadioManager.ProgramInfo> toList() { + synchronized (mLock) { + return mPrograms.values().stream().collect(Collectors.toList()); + } + } + + /** + * Returns the program with a specified primary identifier. + * + * @param id primary identifier of a program to fetch + * @return the program info, or null if there is no such program on the list + */ + public @Nullable RadioManager.ProgramInfo get(@NonNull ProgramSelector.Identifier id) { + synchronized (mLock) { + return mPrograms.get(Objects.requireNonNull(id)); + } + } + + /** + * Filter for the program list. + */ + public static final class Filter implements Parcelable { + private final @NonNull Set<Integer> mIdentifierTypes; + private final @NonNull Set<ProgramSelector.Identifier> mIdentifiers; + private final boolean mIncludeCategories; + private final boolean mExcludeModifications; + private final @Nullable Map<String, String> mVendorFilter; + + /** + * Constructor of program list filter. + * + * Arrays passed to this constructor become owned by this object, do not modify them later. + * + * @param identifierTypes see getIdentifierTypes() + * @param identifiers see getIdentifiers() + * @param includeCategories see areCategoriesIncluded() + * @param excludeModifications see areModificationsExcluded() + */ + public Filter(@NonNull Set<Integer> identifierTypes, + @NonNull Set<ProgramSelector.Identifier> identifiers, + boolean includeCategories, boolean excludeModifications) { + mIdentifierTypes = Objects.requireNonNull(identifierTypes); + mIdentifiers = Objects.requireNonNull(identifiers); + mIncludeCategories = includeCategories; + mExcludeModifications = excludeModifications; + mVendorFilter = null; + } + + /** + * @hide for framework use only + */ + public Filter(@Nullable Map<String, String> vendorFilter) { + mIdentifierTypes = Collections.emptySet(); + mIdentifiers = Collections.emptySet(); + mIncludeCategories = false; + mExcludeModifications = false; + mVendorFilter = vendorFilter; + } + + private Filter(@NonNull Parcel in) { + mIdentifierTypes = Utils.createIntSet(in); + mIdentifiers = Utils.createSet(in, ProgramSelector.Identifier.CREATOR); + mIncludeCategories = in.readByte() != 0; + mExcludeModifications = in.readByte() != 0; + mVendorFilter = Utils.readStringMap(in); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + Utils.writeIntSet(dest, mIdentifierTypes); + Utils.writeSet(dest, mIdentifiers); + dest.writeByte((byte) (mIncludeCategories ? 1 : 0)); + dest.writeByte((byte) (mExcludeModifications ? 1 : 0)); + Utils.writeStringMap(dest, mVendorFilter); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<Filter> CREATOR = new Parcelable.Creator<Filter>() { + public Filter createFromParcel(Parcel in) { + return new Filter(in); + } + + public Filter[] newArray(int size) { + return new Filter[size]; + } + }; + + /** + * @hide for framework use only + */ + public Map<String, String> getVendorFilter() { + return mVendorFilter; + } + + /** + * Returns the list of identifier types that satisfy the filter. + * + * If the program list entry contains at least one identifier of the type + * listed, it satisfies this condition. + * + * Empty list means no filtering on identifier type. + * + * @return the list of accepted identifier types, must not be modified + */ + public @NonNull Set<Integer> getIdentifierTypes() { + return mIdentifierTypes; + } + + /** + * Returns the list of identifiers that satisfy the filter. + * + * If the program list entry contains at least one listed identifier, + * it satisfies this condition. + * + * Empty list means no filtering on identifier. + * + * @return the list of accepted identifiers, must not be modified + */ + public @NonNull Set<ProgramSelector.Identifier> getIdentifiers() { + return mIdentifiers; + } + + /** + * Checks, if non-tunable entries that define tree structure on the + * program list (i.e. DAB ensembles) should be included. + */ + public boolean areCategoriesIncluded() { + return mIncludeCategories; + } + + /** + * Checks, if updates on entry modifications should be disabled. + * + * If true, 'modified' vector of ProgramListChunk must contain list + * additions only. Once the program is added to the list, it's not + * updated anymore. + */ + public boolean areModificationsExcluded() { + return mExcludeModifications; + } + } + + /** + * @hide This is a transport class used for internal communication between + * Broadcast Radio Service and RadioManager. + * Do not use it directly. + */ + public static final class Chunk implements Parcelable { + private final boolean mPurge; + private final boolean mComplete; + private final @NonNull Set<RadioManager.ProgramInfo> mModified; + private final @NonNull Set<ProgramSelector.Identifier> mRemoved; + + public Chunk(boolean purge, boolean complete, + @Nullable Set<RadioManager.ProgramInfo> modified, + @Nullable Set<ProgramSelector.Identifier> removed) { + mPurge = purge; + mComplete = complete; + mModified = (modified != null) ? modified : Collections.emptySet(); + mRemoved = (removed != null) ? removed : Collections.emptySet(); + } + + private Chunk(@NonNull Parcel in) { + mPurge = in.readByte() != 0; + mComplete = in.readByte() != 0; + mModified = Utils.createSet(in, RadioManager.ProgramInfo.CREATOR); + mRemoved = Utils.createSet(in, ProgramSelector.Identifier.CREATOR); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeByte((byte) (mPurge ? 1 : 0)); + dest.writeByte((byte) (mComplete ? 1 : 0)); + Utils.writeSet(dest, mModified); + Utils.writeSet(dest, mRemoved); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<Chunk> CREATOR = new Parcelable.Creator<Chunk>() { + public Chunk createFromParcel(Parcel in) { + return new Chunk(in); + } + + public Chunk[] newArray(int size) { + return new Chunk[size]; + } + }; + + public boolean isPurge() { + return mPurge; + } + + public boolean isComplete() { + return mComplete; + } + + public @NonNull Set<RadioManager.ProgramInfo> getModified() { + return mModified; + } + + public @NonNull Set<ProgramSelector.Identifier> getRemoved() { + return mRemoved; + } + } +} diff --git a/android/hardware/radio/ProgramSelector.java b/android/hardware/radio/ProgramSelector.java index 2211cee9..0294a29b 100644 --- a/android/hardware/radio/ProgramSelector.java +++ b/android/hardware/radio/ProgramSelector.java @@ -27,6 +27,7 @@ import android.os.Parcelable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.stream.Stream; @@ -59,24 +60,58 @@ import java.util.stream.Stream; */ @SystemApi public final class ProgramSelector implements Parcelable { - /** Analogue AM radio (with or without RDS). */ + /** Invalid program type. + * @deprecated use {@link ProgramIdentifier} instead + */ + @Deprecated + public static final int PROGRAM_TYPE_INVALID = 0; + /** Analogue AM radio (with or without RDS). + * @deprecated use {@link ProgramIdentifier} instead + */ + @Deprecated public static final int PROGRAM_TYPE_AM = 1; - /** analogue FM radio (with or without RDS). */ + /** analogue FM radio (with or without RDS). + * @deprecated use {@link ProgramIdentifier} instead + */ + @Deprecated public static final int PROGRAM_TYPE_FM = 2; - /** AM HD Radio. */ + /** AM HD Radio. + * @deprecated use {@link ProgramIdentifier} instead + */ + @Deprecated public static final int PROGRAM_TYPE_AM_HD = 3; - /** FM HD Radio. */ + /** FM HD Radio. + * @deprecated use {@link ProgramIdentifier} instead + */ + @Deprecated public static final int PROGRAM_TYPE_FM_HD = 4; - /** Digital audio broadcasting. */ + /** Digital audio broadcasting. + * @deprecated use {@link ProgramIdentifier} instead + */ + @Deprecated public static final int PROGRAM_TYPE_DAB = 5; - /** Digital Radio Mondiale. */ + /** Digital Radio Mondiale. + * @deprecated use {@link ProgramIdentifier} instead + */ + @Deprecated public static final int PROGRAM_TYPE_DRMO = 6; - /** SiriusXM Satellite Radio. */ + /** SiriusXM Satellite Radio. + * @deprecated use {@link ProgramIdentifier} instead + */ + @Deprecated public static final int PROGRAM_TYPE_SXM = 7; - /** Vendor-specific, not synced across devices. */ + /** Vendor-specific, not synced across devices. + * @deprecated use {@link ProgramIdentifier} instead + */ + @Deprecated public static final int PROGRAM_TYPE_VENDOR_START = 1000; + /** @deprecated use {@link ProgramIdentifier} instead */ + @Deprecated public static final int PROGRAM_TYPE_VENDOR_END = 1999; + /** @deprecated use {@link ProgramIdentifier} instead */ + @Deprecated @IntDef(prefix = { "PROGRAM_TYPE_" }, value = { + PROGRAM_TYPE_INVALID, PROGRAM_TYPE_AM, PROGRAM_TYPE_FM, PROGRAM_TYPE_AM_HD, @@ -89,6 +124,7 @@ public final class ProgramSelector implements Parcelable { @Retention(RetentionPolicy.SOURCE) public @interface ProgramType {} + public static final int IDENTIFIER_TYPE_INVALID = 0; /** kHz */ public static final int IDENTIFIER_TYPE_AMFM_FREQUENCY = 1; /** 16bit */ @@ -109,18 +145,46 @@ public final class ProgramSelector implements Parcelable { * * 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). + * + * @deprecated use IDENTIFIER_TYPE_HD_STATION_ID_EXT instead */ + @Deprecated public static final int IDENTIFIER_TYPE_HD_SUBCHANNEL = 4; /** - * 24bit compound primary identifier for DAB. + * 64bit additional identifier for HD Radio. + * + * Due to Station ID abuse, some HD_STATION_ID_EXT identifiers may be not + * globally unique. To provide a best-effort solution, a short version of + * station name may be carried as additional identifier and may be used + * by the tuner hardware to double-check tuning. + * + * The name is limited to the first 8 A-Z0-9 characters (lowercase letters + * must be converted to uppercase). Encoded in little-endian ASCII: + * the first character of the name is the LSB. + * + * For example: "Abc" is encoded as 0x434241. + */ + public static final int IDENTIFIER_TYPE_HD_STATION_NAME = 10004; + /** + * @see {@link IDENTIFIER_TYPE_DAB_SID_EXT} + */ + public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5; + /** + * 28bit compound primary identifier for Digital Audio Broadcasting. * * Consists of (from the LSB): * - 16bit: SId; - * - 8bit: ECC code. + * - 8bit: ECC code; + * - 4bit: SCIdS. + * + * SCIdS (Service Component Identifier within the Service) value + * of 0 represents the main service, while 1 and above represents + * secondary services. + * * 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; + public static final int IDENTIFIER_TYPE_DAB_SID_EXT = IDENTIFIER_TYPE_DAB_SIDECC; /** 16bit */ public static final int IDENTIFIER_TYPE_DAB_ENSEMBLE = 6; /** 12bit */ @@ -131,7 +195,11 @@ public final class ProgramSelector implements Parcelable { public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9; /** kHz */ public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10; - /** 1: AM, 2:FM */ + /** + * 1: AM, 2:FM + * @deprecated use {@link IDENTIFIER_TYPE_DRMO_FREQUENCY} instead + */ + @Deprecated public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11; /** 32bit */ public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12; @@ -145,13 +213,29 @@ public final class ProgramSelector implements Parcelable { * 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; + public static final int IDENTIFIER_TYPE_VENDOR_START = PROGRAM_TYPE_VENDOR_START; + /** + * @see {@link IDENTIFIER_TYPE_VENDOR_START} + */ + public static final int IDENTIFIER_TYPE_VENDOR_END = PROGRAM_TYPE_VENDOR_END; + /** + * @deprecated use {@link IDENTIFIER_TYPE_VENDOR_START} instead + */ + @Deprecated + public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_START = IDENTIFIER_TYPE_VENDOR_START; + /** + * @deprecated use {@link IDENTIFIER_TYPE_VENDOR_END} instead + */ + @Deprecated + public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_END = IDENTIFIER_TYPE_VENDOR_END; @IntDef(prefix = { "IDENTIFIER_TYPE_" }, value = { + IDENTIFIER_TYPE_INVALID, IDENTIFIER_TYPE_AMFM_FREQUENCY, IDENTIFIER_TYPE_RDS_PI, IDENTIFIER_TYPE_HD_STATION_ID_EXT, IDENTIFIER_TYPE_HD_SUBCHANNEL, + IDENTIFIER_TYPE_HD_STATION_NAME, + IDENTIFIER_TYPE_DAB_SID_EXT, IDENTIFIER_TYPE_DAB_SIDECC, IDENTIFIER_TYPE_DAB_ENSEMBLE, IDENTIFIER_TYPE_DAB_SCID, @@ -162,7 +246,7 @@ public final class ProgramSelector implements Parcelable { IDENTIFIER_TYPE_SXM_SERVICE_ID, IDENTIFIER_TYPE_SXM_CHANNEL, }) - @IntRange(from = IDENTIFIER_TYPE_VENDOR_PRIMARY_START, to = IDENTIFIER_TYPE_VENDOR_PRIMARY_END) + @IntRange(from = IDENTIFIER_TYPE_VENDOR_START, to = IDENTIFIER_TYPE_VENDOR_END) @Retention(RetentionPolicy.SOURCE) public @interface IdentifierType {} @@ -201,7 +285,9 @@ public final class ProgramSelector implements Parcelable { * Type of a radio technology. * * @return program type. + * @deprecated use {@link getPrimaryId} instead */ + @Deprecated public @ProgramType int getProgramType() { return mProgramType; } @@ -268,13 +354,48 @@ public final class ProgramSelector implements Parcelable { * Vendor identifiers are passed as-is to the HAL implementation, * preserving elements order. * - * @return a array of vendor identifiers, must not be modified. + * @return an array of vendor identifiers, must not be modified. + * @deprecated for HAL 1.x compatibility; + * HAL 2.x uses standard primary/secondary lists for vendor IDs */ + @Deprecated public @NonNull long[] getVendorIds() { return mVendorIds; } /** + * Creates an equivalent ProgramSelector with a given secondary identifier preferred. + * + * Used to point to a specific physical identifier for technologies that may broadcast the same + * program on different channels. For example, with a DAB program broadcasted over multiple + * ensembles, the radio hardware may select the one with the strongest signal. The UI may select + * preferred ensemble though, so the radio hardware may try to use it in the first place. + * + * This is a best-effort hint for the tuner, not a guaranteed behavior. + * + * Setting the given secondary identifier as preferred means filtering out other secondary + * identifiers of its type and adding it to the list. + * + * @param preferred preferred secondary identifier + * @return a new ProgramSelector with a given secondary identifier preferred + */ + public @NonNull ProgramSelector withSecondaryPreferred(@NonNull Identifier preferred) { + int preferredType = preferred.getType(); + Identifier[] secondaryIds = Stream.concat( + // remove other identifiers of that type + Arrays.stream(mSecondaryIds).filter(id -> id.getType() != preferredType), + // add preferred identifier instead + Stream.of(preferred)).toArray(Identifier[]::new); + + return new ProgramSelector( + mProgramType, + mPrimaryId, + secondaryIds, + mVendorIds + ); + } + + /** * Builds new ProgramSelector for AM/FM frequency. * * @param band the band. @@ -423,6 +544,10 @@ public final class ProgramSelector implements Parcelable { private final long mValue; public Identifier(@IdentifierType int type, long value) { + if (type == IDENTIFIER_TYPE_HD_STATION_NAME) { + // see getType + type = IDENTIFIER_TYPE_HD_SUBCHANNEL; + } mType = type; mValue = value; } @@ -433,6 +558,13 @@ public final class ProgramSelector implements Parcelable { * @return type of an identifier. */ public @IdentifierType int getType() { + if (mType == IDENTIFIER_TYPE_HD_SUBCHANNEL && mValue > 10) { + /* HD_SUBCHANNEL and HD_STATION_NAME use the same identifier type, but they differ + * in possible values: sub channel is 0-7, station name is greater than ASCII space + * code (32). + */ + return IDENTIFIER_TYPE_HD_STATION_NAME; + } return mType; } diff --git a/android/hardware/radio/RadioManager.java b/android/hardware/radio/RadioManager.java index 4d54e31b..b00f6033 100644 --- a/android/hardware/radio/RadioManager.java +++ b/android/hardware/radio/RadioManager.java @@ -17,8 +17,10 @@ package android.hardware.radio; import android.Manifest; +import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; @@ -32,13 +34,19 @@ import android.os.ServiceManager.ServiceNotFoundException; import android.text.TextUtils; import android.util.Log; +import com.android.internal.util.Preconditions; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.Executor; import java.util.stream.Collectors; /** @@ -119,24 +127,70 @@ public class RadioManager { * @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; - } + /** + * Forces mono audio stream reception. + * + * Analog broadcasts can recover poor reception conditions by jointing + * stereo channels into one. Mainly for, but not limited to AM/FM. + */ + public static final int CONFIG_FORCE_MONO = 1; + /** + * 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 state managed from the HAL implementation side. + * + * Some radio technologies may not support this, ie. DAB. + */ + public static final int CONFIG_FORCE_ANALOG = 2; + /** + * Forces the digital playback for the supporting radio technology. + * + * User may disable digital-analog handover that happens with poor + * reception conditions. With digital forced, the radio will remain silent + * instead of switching to analog channel if it's available. This is purely + * user choice, it does not reflect the actual state of handover. + */ + public static final int CONFIG_FORCE_DIGITAL = 3; + /** + * RDS Alternative Frequencies. + * + * If set and the currently tuned RDS station broadcasts on multiple + * channels, radio tuner automatically switches to the best available + * alternative. + */ + public static final int CONFIG_RDS_AF = 4; + /** + * RDS region-specific program lock-down. + * + * Allows user to lock to the current region as they move into the + * other region. + */ + public static final int CONFIG_RDS_REG = 5; + /** Enables DAB-DAB hard- and implicit-linking (the same content). */ + public static final int CONFIG_DAB_DAB_LINKING = 6; + /** Enables DAB-FM hard- and implicit-linking (the same content). */ + public static final int CONFIG_DAB_FM_LINKING = 7; + /** Enables DAB-DAB soft-linking (related content). */ + public static final int CONFIG_DAB_DAB_SOFT_LINKING = 8; + /** Enables DAB-FM soft-linking (related content). */ + public static final int CONFIG_DAB_FM_SOFT_LINKING = 9; + + /** @hide */ + @IntDef(prefix = { "CONFIG_" }, value = { + CONFIG_FORCE_MONO, + CONFIG_FORCE_ANALOG, + CONFIG_FORCE_DIGITAL, + CONFIG_RDS_AF, + CONFIG_RDS_REG, + CONFIG_DAB_DAB_LINKING, + CONFIG_DAB_FM_LINKING, + CONFIG_DAB_DAB_SOFT_LINKING, + CONFIG_DAB_FM_SOFT_LINKING, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ConfigFlag {} /***************************************************************************** * Lists properties, options and radio bands supported by a given broadcast radio module. @@ -349,7 +403,7 @@ public class RadioManager { mIsBgScanSupported = in.readInt() == 1; mSupportedProgramTypes = arrayToSet(in.createIntArray()); mSupportedIdentifierTypes = arrayToSet(in.createIntArray()); - mVendorInfo = readStringMap(in); + mVendorInfo = Utils.readStringMap(in); } public static final Parcelable.Creator<ModuleProperties> CREATOR @@ -379,7 +433,7 @@ public class RadioManager { dest.writeInt(mIsBgScanSupported ? 1 : 0); dest.writeIntArray(setToArray(mSupportedProgramTypes)); dest.writeIntArray(setToArray(mSupportedIdentifierTypes)); - writeStringMap(dest, mVendorInfo); + Utils.writeStringMap(dest, mVendorInfo); } @Override @@ -645,7 +699,8 @@ public class RadioManager { private final boolean mAf; private final boolean mEa; - FmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing, + /** @hide */ + public 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; @@ -771,7 +826,8 @@ public class RadioManager { private final boolean mStereo; - AmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing, + /** @hide */ + public AmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing, boolean stereo) { super(region, type, lowerLimit, upperLimit, spacing); mStereo = stereo; @@ -843,10 +899,10 @@ public class RadioManager { /** Radio band configuration. */ public static class BandConfig implements Parcelable { - final BandDescriptor mDescriptor; + @NonNull final BandDescriptor mDescriptor; BandConfig(BandDescriptor descriptor) { - mDescriptor = descriptor; + mDescriptor = Objects.requireNonNull(descriptor); } BandConfig(int region, int type, int lowerLimit, int upperLimit, int spacing) { @@ -968,7 +1024,8 @@ public class RadioManager { private final boolean mAf; private final boolean mEa; - FmBandConfig(FmBandDescriptor descriptor) { + /** @hide */ + public FmBandConfig(FmBandDescriptor descriptor) { super((BandDescriptor)descriptor); mStereo = descriptor.isStereoSupported(); mRds = descriptor.isRdsSupported(); @@ -1204,7 +1261,8 @@ public class RadioManager { public static class AmBandConfig extends BandConfig { private final boolean mStereo; - AmBandConfig(AmBandDescriptor descriptor) { + /** @hide */ + public AmBandConfig(AmBandDescriptor descriptor) { super((BandDescriptor)descriptor); mStereo = descriptor.isStereoSupported(); } @@ -1329,34 +1387,44 @@ public class RadioManager { }; } - /** Radio program information returned by - * {@link RadioTuner#getProgramInformation(RadioManager.ProgramInfo[])} */ + /** Radio program information. */ public static class ProgramInfo implements Parcelable { - // sourced from hardware/interfaces/broadcastradio/1.1/types.hal + // sourced from hardware/interfaces/broadcastradio/2.0/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; + private static final int FLAG_TUNED = 1 << 4; + private static final int FLAG_STEREO = 1 << 5; @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; + @Nullable private final ProgramSelector.Identifier mLogicallyTunedTo; + @Nullable private final ProgramSelector.Identifier mPhysicallyTunedTo; + @NonNull private final Collection<ProgramSelector.Identifier> mRelatedContent; + private final int mInfoFlags; + private final int mSignalQuality; + @Nullable 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; + /** @hide */ + public ProgramInfo(@NonNull ProgramSelector selector, + @Nullable ProgramSelector.Identifier logicallyTunedTo, + @Nullable ProgramSelector.Identifier physicallyTunedTo, + @Nullable Collection<ProgramSelector.Identifier> relatedContent, + int infoFlags, int signalQuality, @Nullable RadioMetadata metadata, + @Nullable Map<String, String> vendorInfo) { + mSelector = Objects.requireNonNull(selector); + mLogicallyTunedTo = logicallyTunedTo; + mPhysicallyTunedTo = physicallyTunedTo; + if (relatedContent == null) { + mRelatedContent = Collections.emptyList(); + } else { + Preconditions.checkCollectionElementsNotNull(relatedContent, "relatedContent"); + mRelatedContent = relatedContent; + } + mInfoFlags = infoFlags; + mSignalQuality = signalQuality; mMetadata = metadata; mVendorInfo = (vendorInfo == null) ? new HashMap<>() : vendorInfo; } @@ -1370,6 +1438,51 @@ public class RadioManager { return mSelector; } + /** + * Identifier currently used for program selection. + * + * This identifier can be used to determine which technology is + * currently being used for reception. + * + * Some program selectors contain tuning information for different radio + * technologies (i.e. FM RDS and DAB). For example, user may tune using + * a ProgramSelector with RDS_PI primary identifier, but the tuner hardware + * may choose to use DAB technology to make actual tuning. This identifier + * must reflect that. + */ + public @Nullable ProgramSelector.Identifier getLogicallyTunedTo() { + return mLogicallyTunedTo; + } + + /** + * Identifier currently used by hardware to physically tune to a channel. + * + * Some radio technologies broadcast the same program on multiple channels, + * i.e. with RDS AF the same program may be broadcasted on multiple + * alternative frequencies; the same DAB program may be broadcast on + * multiple ensembles. This identifier points to the channel to which the + * radio hardware is physically tuned to. + */ + public @Nullable ProgramSelector.Identifier getPhysicallyTunedTo() { + return mPhysicallyTunedTo; + } + + /** + * Primary identifiers of related contents. + * + * Some radio technologies provide pointers to other programs that carry + * related content (i.e. DAB soft-links). This field is a list of pointers + * to other programs on the program list. + * + * Please note, that these identifiers does not have to exist on the program + * list - i.e. DAB tuner may provide information on FM RDS alternatives + * despite not supporting FM RDS. If the system has multiple tuners, another + * one may have it on its list. + */ + public @Nullable Collection<ProgramSelector.Identifier> getRelatedContent() { + return mRelatedContent; + } + /** Main channel expressed in units according to band type. * Currently all defined band types express channels as frequency in kHz * @return the program channel @@ -1404,19 +1517,28 @@ public class RadioManager { * @return {@code true} if currently tuned, {@code false} otherwise. */ public boolean isTuned() { - return mTuned; + return (mInfoFlags & FLAG_TUNED) != 0; } + /** {@code true} if the received program is stereo * @return {@code true} if stereo, {@code false} otherwise. */ public boolean isStereo() { - return mStereo; + return (mInfoFlags & FLAG_STEREO) != 0; } + /** {@code true} if the received program is digital (e.g HD radio) * @return {@code true} if digital, {@code false} otherwise. + * @deprecated Use {@link getLogicallyTunedTo()} instead. */ + @Deprecated public boolean isDigital() { - return mDigital; + ProgramSelector.Identifier id = mLogicallyTunedTo; + if (id == null) id = mSelector.getPrimaryId(); + + int type = id.getType(); + return (type != ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY + && type != ProgramSelector.IDENTIFIER_TYPE_RDS_PI); } /** @@ -1425,7 +1547,7 @@ public class RadioManager { * usually targetted at reduced latency. */ public boolean isLive() { - return (mFlags & FLAG_LIVE) != 0; + return (mInfoFlags & FLAG_LIVE) != 0; } /** @@ -1435,7 +1557,7 @@ public class RadioManager { * It does NOT mean the user has muted audio. */ public boolean isMuted() { - return (mFlags & FLAG_MUTED) != 0; + return (mInfoFlags & FLAG_MUTED) != 0; } /** @@ -1443,7 +1565,7 @@ public class RadioManager { * regularily. */ public boolean isTrafficProgram() { - return (mFlags & FLAG_TRAFFIC_PROGRAM) != 0; + return (mInfoFlags & FLAG_TRAFFIC_PROGRAM) != 0; } /** @@ -1451,15 +1573,18 @@ public class RadioManager { * at the very moment. */ public boolean isTrafficAnnouncementActive() { - return (mFlags & FLAG_TRAFFIC_ANNOUNCEMENT) != 0; + return (mInfoFlags & FLAG_TRAFFIC_ANNOUNCEMENT) != 0; } - /** Signal strength indicator from 0 (no signal) to 100 (excellent) - * @return the signal strength indication. + /** + * Signal quality (as opposed to the name) indication from 0 (no signal) + * to 100 (excellent) + * @return the signal quality indication. */ public int getSignalStrength() { - return mSignalStrength; + return mSignalQuality; } + /** Metadata currently received from this station. * null if no metadata have been received * @return current meta data received from this program. @@ -1483,18 +1608,14 @@ public class RadioManager { } 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); + mSelector = Objects.requireNonNull(in.readTypedObject(ProgramSelector.CREATOR)); + mLogicallyTunedTo = in.readTypedObject(ProgramSelector.Identifier.CREATOR); + mPhysicallyTunedTo = in.readTypedObject(ProgramSelector.Identifier.CREATOR); + mRelatedContent = in.createTypedArrayList(ProgramSelector.Identifier.CREATOR); + mInfoFlags = in.readInt(); + mSignalQuality = in.readInt(); + mMetadata = in.readTypedObject(RadioMetadata.CREATOR); + mVendorInfo = Utils.readStringMap(in); } public static final Parcelable.Creator<ProgramInfo> CREATOR @@ -1510,19 +1631,14 @@ public class RadioManager { @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); + dest.writeTypedObject(mSelector, flags); + dest.writeTypedObject(mLogicallyTunedTo, flags); + dest.writeTypedObject(mPhysicallyTunedTo, flags); + Utils.writeTypedCollection(dest, mRelatedContent); + dest.writeInt(mInfoFlags); + dest.writeInt(mSignalQuality); + dest.writeTypedObject(mMetadata, flags); + Utils.writeStringMap(dest, mVendorInfo); } @Override @@ -1532,52 +1648,38 @@ public class RadioManager { @Override public String toString() { - return "ProgramInfo [mSelector=" + mSelector - + ", mTuned=" + mTuned + ", mStereo=" + mStereo + ", mDigital=" + mDigital - + ", mFlags=" + mFlags + ", mSignalStrength=" + mSignalStrength - + ((mMetadata == null) ? "" : (", mMetadata=" + mMetadata.toString())) + return "ProgramInfo" + + " [selector=" + mSelector + + ", logicallyTunedTo=" + Objects.toString(mLogicallyTunedTo) + + ", physicallyTunedTo=" + Objects.toString(mPhysicallyTunedTo) + + ", relatedContent=" + mRelatedContent.size() + + ", infoFlags=" + mInfoFlags + + ", mSignalQuality=" + mSignalQuality + + ", mMetadata=" + Objects.toString(mMetadata) + "]"; } @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; + return Objects.hash(mSelector, mLogicallyTunedTo, mPhysicallyTunedTo, + mRelatedContent, mInfoFlags, mSignalQuality, mMetadata, mVendorInfo); } @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (!(obj instanceof ProgramInfo)) - return false; + 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; + + if (!Objects.equals(mSelector, other.mSelector)) return false; + if (!Objects.equals(mLogicallyTunedTo, other.mLogicallyTunedTo)) return false; + if (!Objects.equals(mPhysicallyTunedTo, other.mPhysicallyTunedTo)) return false; + if (!Objects.equals(mRelatedContent, other.mRelatedContent)) return false; + if (mInfoFlags != other.mInfoFlags) return false; + if (mSignalQuality != other.mSignalQuality) return false; + if (!Objects.equals(mMetadata, other.mMetadata)) return false; + if (!Objects.equals(mVendorInfo, other.mVendorInfo)) return false; + return true; } } @@ -1649,15 +1751,78 @@ public class RadioManager { 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); + } catch (RemoteException | IllegalArgumentException ex) { + Log.e(TAG, "Failed to open tuner", ex); return null; } if (tuner == null) { Log.e(TAG, "Failed to open tuner"); return null; } - return new TunerAdapter(tuner, config != null ? config.getType() : BAND_INVALID); + return new TunerAdapter(tuner, halCallback, + config != null ? config.getType() : BAND_INVALID); + } + + private final Map<Announcement.OnListUpdatedListener, ICloseHandle> mAnnouncementListeners = + new HashMap<>(); + + /** + * Adds new announcement listener. + * + * @param enabledAnnouncementTypes a set of announcement types to listen to + * @param listener announcement listener + */ + @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO) + public void addAnnouncementListener(@NonNull Set<Integer> enabledAnnouncementTypes, + @NonNull Announcement.OnListUpdatedListener listener) { + addAnnouncementListener(cmd -> cmd.run(), enabledAnnouncementTypes, listener); + } + + /** + * Adds new announcement listener with executor. + * + * @param executor the executor + * @param enabledAnnouncementTypes a set of announcement types to listen to + * @param listener announcement listener + */ + @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO) + public void addAnnouncementListener(@NonNull @CallbackExecutor Executor executor, + @NonNull Set<Integer> enabledAnnouncementTypes, + @NonNull Announcement.OnListUpdatedListener listener) { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + int[] types = enabledAnnouncementTypes.stream().mapToInt(Integer::intValue).toArray(); + IAnnouncementListener listenerIface = new IAnnouncementListener.Stub() { + public void onListUpdated(List<Announcement> activeAnnouncements) { + executor.execute(() -> listener.onListUpdated(activeAnnouncements)); + } + }; + synchronized (mAnnouncementListeners) { + ICloseHandle closeHandle = null; + try { + closeHandle = mService.addAnnouncementListener(types, listenerIface); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } + Objects.requireNonNull(closeHandle); + ICloseHandle oldCloseHandle = mAnnouncementListeners.put(listener, closeHandle); + if (oldCloseHandle != null) Utils.close(oldCloseHandle); + } + } + + /** + * Removes previously registered announcement listener. + * + * @param listener announcement listener, previously registered with + * {@link addAnnouncementListener} + */ + @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO) + public void removeAnnouncementListener(@NonNull Announcement.OnListUpdatedListener listener) { + Objects.requireNonNull(listener); + synchronized (mAnnouncementListeners) { + ICloseHandle closeHandle = mAnnouncementListeners.remove(listener); + if (closeHandle != null) Utils.close(closeHandle); + } } @NonNull private final Context mContext; diff --git a/android/hardware/radio/RadioTuner.java b/android/hardware/radio/RadioTuner.java index e93fd5f1..ed20c4aa 100644 --- a/android/hardware/radio/RadioTuner.java +++ b/android/hardware/radio/RadioTuner.java @@ -280,17 +280,37 @@ public abstract class RadioTuner { * @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. + * @deprecated Use {@link getDynamicProgramList} instead. */ + @Deprecated public abstract @NonNull List<RadioManager.ProgramInfo> getProgramList(@Nullable Map<String, String> vendorFilter); /** + * Get the dynamic list of discovered radio stations. + * + * The list object is updated asynchronously; to get the updates register + * with {@link ProgramList#addListCallback}. + * + * When the returned object is no longer used, it must be closed. + * + * @param filter filter for the list, or null to get the full list. + * @return the dynamic program list object, close it after use + * or {@code null} if program list is not supported by the tuner + */ + public @Nullable ProgramList getDynamicProgramList(@Nullable ProgramList.Filter filter) { + return null; + } + + /** * 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. + * @deprecated Use {@link isConfigFlagSet(int)} instead. */ + @Deprecated public abstract boolean isAnalogForced(); /** @@ -305,10 +325,50 @@ public abstract class RadioTuner { * @param isForced {@code true} to force analog, {@code false} for a default behaviour. * @throws IllegalStateException if the switch is not supported at current * configuration. + * @deprecated Use {@link setConfigFlag(int, boolean)} instead. */ + @Deprecated public abstract void setAnalogForced(boolean isForced); /** + * Checks, if a given config flag is supported + * + * @param flag Flag to check. + * @return True, if the flag is supported. + */ + public boolean isConfigFlagSupported(@RadioManager.ConfigFlag int flag) { + return false; + } + + /** + * Fetches the current setting of a given config flag. + * + * The success/failure result is consistent with isConfigFlagSupported. + * + * @param flag Flag to fetch. + * @return The current value of the flag. + * @throws IllegalStateException if the flag is not applicable right now. + * @throws UnsupportedOperationException if the flag is not supported at all. + */ + public boolean isConfigFlagSet(@RadioManager.ConfigFlag int flag) { + throw new UnsupportedOperationException(); + } + + /** + * Sets the config flag. + * + * The success/failure result is consistent with isConfigFlagSupported. + * + * @param flag Flag to set. + * @param value The new value of a given flag. + * @throws IllegalStateException if the flag is not applicable right now. + * @throws UnsupportedOperationException if the flag is not supported at all. + */ + public void setConfigFlag(@RadioManager.ConfigFlag int flag, boolean value) { + throw new UnsupportedOperationException(); + } + + /** * Generic method for setting vendor-specific parameter values. * The framework does not interpret the parameters, they are passed * in an opaque manner between a vendor application and HAL. @@ -316,6 +376,7 @@ public abstract class RadioTuner { * Framework does not make any assumptions on the keys or values, other than * ones stated in VendorKeyValue documentation (a requirement of key * prefixes). + * See VendorKeyValue at hardware/interfaces/broadcastradio/2.0/types.hal. * * For each pair in the result map, the key will be one of the keys * contained in the input (possibly with wildcards expanded), and the value @@ -332,10 +393,11 @@ public abstract class RadioTuner { * * @param parameters Vendor-specific key-value pairs. * @return Operation completion status for parameters being set. - * @hide FutureFeature */ - public abstract @NonNull Map<String, String> - setParameters(@NonNull Map<String, String> parameters); + public @NonNull Map<String, String> + setParameters(@NonNull Map<String, String> parameters) { + throw new UnsupportedOperationException(); + } /** * Generic method for retrieving vendor-specific parameter values. @@ -355,10 +417,11 @@ public abstract class RadioTuner { * * @param keys Parameter keys to fetch. * @return Vendor-specific key-value pairs. - * @hide FutureFeature */ - public abstract @NonNull Map<String, String> - getParameters(@NonNull List<String> keys); + public @NonNull Map<String, String> + getParameters(@NonNull List<String> keys) { + throw new UnsupportedOperationException(); + } /** * Get current antenna connection state for current configuration. @@ -494,7 +557,6 @@ public abstract class RadioTuner { * asynchronously. * * @param parameters Vendor-specific key-value pairs. - * @hide FutureFeature */ public void onParametersUpdated(@NonNull Map<String, String> parameters) {} } diff --git a/android/hardware/radio/TunerAdapter.java b/android/hardware/radio/TunerAdapter.java index 864d17c2..91944bfd 100644 --- a/android/hardware/radio/TunerAdapter.java +++ b/android/hardware/radio/TunerAdapter.java @@ -33,15 +33,18 @@ class TunerAdapter extends RadioTuner { private static final String TAG = "BroadcastRadio.TunerAdapter"; @NonNull private final ITuner mTuner; + @NonNull private final TunerCallbackAdapter mCallback; private boolean mIsClosed = false; private @RadioManager.Band int mBand; - TunerAdapter(ITuner tuner, @RadioManager.Band int band) { - if (tuner == null) { - throw new NullPointerException(); - } - mTuner = tuner; + private ProgramList mLegacyListProxy; + private Map<String, String> mLegacyListFilter; + + TunerAdapter(@NonNull ITuner tuner, @NonNull TunerCallbackAdapter callback, + @RadioManager.Band int band) { + mTuner = Objects.requireNonNull(tuner); + mCallback = Objects.requireNonNull(callback); mBand = band; } @@ -53,6 +56,10 @@ class TunerAdapter extends RadioTuner { return; } mIsClosed = true; + if (mLegacyListProxy != null) { + mLegacyListProxy.close(); + mLegacyListProxy = null; + } } try { mTuner.close(); @@ -63,6 +70,7 @@ class TunerAdapter extends RadioTuner { @Override public int setConfiguration(RadioManager.BandConfig config) { + if (config == null) return RadioManager.STATUS_BAD_VALUE; try { mTuner.setConfiguration(config); mBand = config.getType(); @@ -226,26 +234,90 @@ class TunerAdapter extends RadioTuner { @Override public @NonNull List<RadioManager.ProgramInfo> getProgramList(@Nullable Map<String, String> vendorFilter) { + synchronized (mTuner) { + if (mLegacyListProxy == null || !Objects.equals(mLegacyListFilter, vendorFilter)) { + Log.i(TAG, "Program list filter has changed, requesting new list"); + mLegacyListProxy = new ProgramList(); + mLegacyListFilter = vendorFilter; + + mCallback.clearLastCompleteList(); + mCallback.setProgramListObserver(mLegacyListProxy, () -> { }); + try { + mTuner.startProgramListUpdates(new ProgramList.Filter(vendorFilter)); + } catch (RemoteException ex) { + throw new RuntimeException("service died", ex); + } + } + + List<RadioManager.ProgramInfo> list = mCallback.getLastCompleteList(); + if (list == null) throw new IllegalStateException("Program list is not ready yet"); + return list; + } + } + + @Override + public @Nullable ProgramList getDynamicProgramList(@Nullable ProgramList.Filter filter) { + synchronized (mTuner) { + if (mLegacyListProxy != null) { + mLegacyListProxy.close(); + mLegacyListProxy = null; + } + mLegacyListFilter = null; + + ProgramList list = new ProgramList(); + mCallback.setProgramListObserver(list, () -> { + try { + mTuner.stopProgramListUpdates(); + } catch (RemoteException ex) { + Log.e(TAG, "Couldn't stop program list updates", ex); + } + }); + + try { + mTuner.startProgramListUpdates(filter); + } catch (UnsupportedOperationException ex) { + return null; + } catch (RemoteException ex) { + mCallback.setProgramListObserver(null, () -> { }); + throw new RuntimeException("service died", ex); + } + + return list; + } + } + + @Override + public boolean isAnalogForced() { + return isConfigFlagSet(RadioManager.CONFIG_FORCE_ANALOG); + } + + @Override + public void setAnalogForced(boolean isForced) { + setConfigFlag(RadioManager.CONFIG_FORCE_ANALOG, isForced); + } + + @Override + public boolean isConfigFlagSupported(@RadioManager.ConfigFlag int flag) { try { - return mTuner.getProgramList(vendorFilter); + return mTuner.isConfigFlagSupported(flag); } catch (RemoteException e) { throw new RuntimeException("service died", e); } } @Override - public boolean isAnalogForced() { + public boolean isConfigFlagSet(@RadioManager.ConfigFlag int flag) { try { - return mTuner.isAnalogForced(); + return mTuner.isConfigFlagSet(flag); } catch (RemoteException e) { throw new RuntimeException("service died", e); } } @Override - public void setAnalogForced(boolean isForced) { + public void setConfigFlag(@RadioManager.ConfigFlag int flag, boolean value) { try { - mTuner.setAnalogForced(isForced); + mTuner.setConfigFlag(flag, value); } catch (RemoteException e) { throw new RuntimeException("service died", e); } diff --git a/android/hardware/radio/TunerCallbackAdapter.java b/android/hardware/radio/TunerCallbackAdapter.java index a01f658e..b299ffe0 100644 --- a/android/hardware/radio/TunerCallbackAdapter.java +++ b/android/hardware/radio/TunerCallbackAdapter.java @@ -22,7 +22,9 @@ import android.os.Handler; import android.os.Looper; import android.util.Log; +import java.util.List; import java.util.Map; +import java.util.Objects; /** * Implements the ITunerCallback interface by forwarding calls to RadioTuner.Callback. @@ -30,9 +32,14 @@ import java.util.Map; class TunerCallbackAdapter extends ITunerCallback.Stub { private static final String TAG = "BroadcastRadio.TunerCallbackAdapter"; + private final Object mLock = new Object(); @NonNull private final RadioTuner.Callback mCallback; @NonNull private final Handler mHandler; + @Nullable ProgramList mProgramList; + @Nullable List<RadioManager.ProgramInfo> mLastCompleteList; // for legacy getProgramList call + private boolean mDelayedCompleteCallback = false; + TunerCallbackAdapter(@NonNull RadioTuner.Callback callback, @Nullable Handler handler) { mCallback = callback; if (handler == null) { @@ -42,6 +49,49 @@ class TunerCallbackAdapter extends ITunerCallback.Stub { } } + void setProgramListObserver(@Nullable ProgramList programList, + @NonNull ProgramList.OnCloseListener closeListener) { + Objects.requireNonNull(closeListener); + synchronized (mLock) { + if (mProgramList != null) { + Log.w(TAG, "Previous program list observer wasn't properly closed, closing it..."); + mProgramList.close(); + } + mProgramList = programList; + if (programList == null) return; + programList.setOnCloseListener(() -> { + synchronized (mLock) { + if (mProgramList != programList) return; + mProgramList = null; + mLastCompleteList = null; + closeListener.onClose(); + } + }); + programList.addOnCompleteListener(() -> { + synchronized (mLock) { + if (mProgramList != programList) return; + mLastCompleteList = programList.toList(); + if (mDelayedCompleteCallback) { + Log.d(TAG, "Sending delayed onBackgroundScanComplete callback"); + sendBackgroundScanCompleteLocked(); + } + } + }); + } + } + + @Nullable List<RadioManager.ProgramInfo> getLastCompleteList() { + synchronized (mLock) { + return mLastCompleteList; + } + } + + void clearLastCompleteList() { + synchronized (mLock) { + mLastCompleteList = null; + } + } + @Override public void onError(int status) { mHandler.post(() -> mCallback.onError(status)); @@ -87,9 +137,22 @@ class TunerCallbackAdapter extends ITunerCallback.Stub { mHandler.post(() -> mCallback.onBackgroundScanAvailabilityChange(isAvailable)); } + private void sendBackgroundScanCompleteLocked() { + mDelayedCompleteCallback = false; + mHandler.post(() -> mCallback.onBackgroundScanComplete()); + } + @Override public void onBackgroundScanComplete() { - mHandler.post(() -> mCallback.onBackgroundScanComplete()); + synchronized (mLock) { + if (mLastCompleteList == null) { + Log.i(TAG, "Got onBackgroundScanComplete callback, but the " + + "program list didn't get through yet. Delaying it..."); + mDelayedCompleteCallback = true; + return; + } + sendBackgroundScanCompleteLocked(); + } } @Override @@ -98,6 +161,14 @@ class TunerCallbackAdapter extends ITunerCallback.Stub { } @Override + public void onProgramListUpdated(ProgramList.Chunk chunk) { + synchronized (mLock) { + if (mProgramList == null) return; + mProgramList.apply(Objects.requireNonNull(chunk)); + } + } + + @Override public void onParametersUpdated(Map parameters) { mHandler.post(() -> mCallback.onParametersUpdated(parameters)); } diff --git a/android/hardware/radio/Utils.java b/android/hardware/radio/Utils.java new file mode 100644 index 00000000..f1b58974 --- /dev/null +++ b/android/hardware/radio/Utils.java @@ -0,0 +1,118 @@ +/** + * Copyright (C) 2018 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.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +final class Utils { + private static final String TAG = "BroadcastRadio.utils"; + + static void writeStringMap(@NonNull Parcel dest, @Nullable Map<String, String> map) { + if (map == null) { + dest.writeInt(0); + return; + } + dest.writeInt(map.size()); + for (Map.Entry<String, String> entry : map.entrySet()) { + dest.writeString(entry.getKey()); + dest.writeString(entry.getValue()); + } + } + + 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; + } + + static <T extends Parcelable> void writeSet(@NonNull Parcel dest, @Nullable Set<T> set) { + if (set == null) { + dest.writeInt(0); + return; + } + dest.writeInt(set.size()); + set.stream().forEach(elem -> dest.writeTypedObject(elem, 0)); + } + + static <T> Set<T> createSet(@NonNull Parcel in, Parcelable.Creator<T> c) { + int size = in.readInt(); + Set<T> set = new HashSet<>(); + while (size-- > 0) { + set.add(in.readTypedObject(c)); + } + return set; + } + + static void writeIntSet(@NonNull Parcel dest, @Nullable Set<Integer> set) { + if (set == null) { + dest.writeInt(0); + return; + } + dest.writeInt(set.size()); + set.stream().forEach(elem -> dest.writeInt(Objects.requireNonNull(elem))); + } + + static Set<Integer> createIntSet(@NonNull Parcel in) { + return createSet(in, new Parcelable.Creator<Integer>() { + public Integer createFromParcel(Parcel in) { + return in.readInt(); + } + + public Integer[] newArray(int size) { + return new Integer[size]; + } + }); + } + + static <T extends Parcelable> void writeTypedCollection(@NonNull Parcel dest, + @Nullable Collection<T> coll) { + ArrayList<T> list = null; + if (coll != null) { + if (coll instanceof ArrayList) { + list = (ArrayList) coll; + } else { + list = new ArrayList<>(coll); + } + } + dest.writeTypedList(list); + } + + static void close(ICloseHandle handle) { + try { + handle.close(); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } + } +} |