summaryrefslogtreecommitdiff
path: root/core/java
diff options
context:
space:
mode:
Diffstat (limited to 'core/java')
-rw-r--r--core/java/android/hardware/radio/ITuner.aidl11
-rw-r--r--core/java/android/hardware/radio/ITunerCallback.aidl2
-rw-r--r--core/java/android/hardware/radio/ProgramList.aidl23
-rw-r--r--core/java/android/hardware/radio/ProgramList.java427
-rw-r--r--core/java/android/hardware/radio/ProgramSelector.java6
-rw-r--r--core/java/android/hardware/radio/RadioManager.java35
-rw-r--r--core/java/android/hardware/radio/RadioTuner.java18
-rw-r--r--core/java/android/hardware/radio/TunerAdapter.java70
-rw-r--r--core/java/android/hardware/radio/TunerCallbackAdapter.java73
-rw-r--r--core/java/android/hardware/radio/Utils.java92
10 files changed, 712 insertions, 45 deletions
diff --git a/core/java/android/hardware/radio/ITuner.aidl b/core/java/android/hardware/radio/ITuner.aidl
index ca380769954b..bf5e391794f5 100644
--- a/core/java/android/hardware/radio/ITuner.aidl
+++ b/core/java/android/hardware/radio/ITuner.aidl
@@ -17,6 +17,7 @@
package android.hardware.radio;
import android.graphics.Bitmap;
+import android.hardware.radio.ProgramList;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
@@ -73,14 +74,8 @@ interface ITuner {
*/
boolean startBackgroundScan();
- /**
- * @param vendorFilter Vendor-specific filter, must be Map<String, String>
- * @return the list, or null if scan is in progress
- * @throws IllegalArgumentException if invalid arguments are passed
- * @throws IllegalStateException if the scan has not been started, client may
- * call startBackgroundScan to fix this.
- */
- List<RadioManager.ProgramInfo> getProgramList(in Map vendorFilter);
+ void startProgramListUpdates(in ProgramList.Filter filter);
+ void stopProgramListUpdates();
boolean isConfigFlagSupported(int flag);
boolean isConfigFlagSet(int flag);
diff --git a/core/java/android/hardware/radio/ITunerCallback.aidl b/core/java/android/hardware/radio/ITunerCallback.aidl
index 775e25c7e7cf..54af30fcc35e 100644
--- a/core/java/android/hardware/radio/ITunerCallback.aidl
+++ b/core/java/android/hardware/radio/ITunerCallback.aidl
@@ -16,6 +16,7 @@
package android.hardware.radio;
+import android.hardware.radio.ProgramList;
import android.hardware.radio.RadioManager;
import android.hardware.radio.RadioMetadata;
@@ -30,6 +31,7 @@ oneway interface ITunerCallback {
void onBackgroundScanAvailabilityChange(boolean isAvailable);
void onBackgroundScanComplete();
void onProgramListChanged();
+ void onProgramListUpdated(in ProgramList.Chunk chunk);
/**
* @param parameters Vendor-specific key-value pairs, must be Map<String, String>
diff --git a/core/java/android/hardware/radio/ProgramList.aidl b/core/java/android/hardware/radio/ProgramList.aidl
new file mode 100644
index 000000000000..34b7f97558c7
--- /dev/null
+++ b/core/java/android/hardware/radio/ProgramList.aidl
@@ -0,0 +1,23 @@
+/**
+ * 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;
+
+/** @hide */
+parcelable ProgramList.Filter;
+
+/** @hide */
+parcelable ProgramList.Chunk;
diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java
new file mode 100644
index 000000000000..b2aa9ba532a9
--- /dev/null
+++ b/core/java/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/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java
index 2211cee9b315..3556751f4af4 100644
--- a/core/java/android/hardware/radio/ProgramSelector.java
+++ b/core/java/android/hardware/radio/ProgramSelector.java
@@ -59,6 +59,7 @@ import java.util.stream.Stream;
*/
@SystemApi
public final class ProgramSelector implements Parcelable {
+ public static final int PROGRAM_TYPE_INVALID = 0;
/** Analogue AM radio (with or without RDS). */
public static final int PROGRAM_TYPE_AM = 1;
/** analogue FM radio (with or without RDS). */
@@ -77,6 +78,7 @@ public final class ProgramSelector implements Parcelable {
public static final int PROGRAM_TYPE_VENDOR_START = 1000;
public static final int PROGRAM_TYPE_VENDOR_END = 1999;
@IntDef(prefix = { "PROGRAM_TYPE_" }, value = {
+ PROGRAM_TYPE_INVALID,
PROGRAM_TYPE_AM,
PROGRAM_TYPE_FM,
PROGRAM_TYPE_AM_HD,
@@ -89,6 +91,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 */
@@ -148,6 +151,7 @@ public final class ProgramSelector implements Parcelable {
public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_START = PROGRAM_TYPE_VENDOR_START;
public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_END = PROGRAM_TYPE_VENDOR_END;
@IntDef(prefix = { "IDENTIFIER_TYPE_" }, value = {
+ IDENTIFIER_TYPE_INVALID,
IDENTIFIER_TYPE_AMFM_FREQUENCY,
IDENTIFIER_TYPE_RDS_PI,
IDENTIFIER_TYPE_HD_STATION_ID_EXT,
@@ -268,7 +272,7 @@ 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.
*/
public @NonNull long[] getVendorIds() {
return mVendorIds;
diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java
index b740f1430157..56668ac00527 100644
--- a/core/java/android/hardware/radio/RadioManager.java
+++ b/core/java/android/hardware/radio/RadioManager.java
@@ -185,25 +185,6 @@ public class RadioManager {
@Retention(RetentionPolicy.SOURCE)
public @interface ConfigFlag {}
- private static void writeStringMap(@NonNull Parcel dest, @NonNull Map<String, String> map) {
- dest.writeInt(map.size());
- for (Map.Entry<String, String> entry : map.entrySet()) {
- dest.writeString(entry.getKey());
- dest.writeString(entry.getValue());
- }
- }
-
- private static @NonNull Map<String, String> readStringMap(@NonNull Parcel in) {
- int size = in.readInt();
- Map<String, String> map = new HashMap<>();
- while (size-- > 0) {
- String key = in.readString();
- String value = in.readString();
- map.put(key, value);
- }
- return map;
- }
-
/*****************************************************************************
* Lists properties, options and radio bands supported by a given broadcast radio module.
* Each module has a unique ID used to address it when calling RadioManager APIs.
@@ -415,7 +396,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
@@ -445,7 +426,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
@@ -1410,7 +1391,7 @@ public class RadioManager {
private static final int FLAG_TRAFFIC_ANNOUNCEMENT = 1 << 3;
@NonNull private final ProgramSelector mSelector;
- private final boolean mTuned;
+ private final boolean mTuned; // TODO(b/69958777): replace with mFlags
private final boolean mStereo;
private final boolean mDigital;
private final int mFlags;
@@ -1418,7 +1399,8 @@ public class RadioManager {
private final RadioMetadata mMetadata;
@NonNull private final Map<String, String> mVendorInfo;
- ProgramInfo(@NonNull ProgramSelector selector, boolean tuned, boolean stereo,
+ /** @hide */
+ public ProgramInfo(@NonNull ProgramSelector selector, boolean tuned, boolean stereo,
boolean digital, int signalStrength, RadioMetadata metadata, int flags,
Map<String, String> vendorInfo) {
mSelector = selector;
@@ -1564,7 +1546,7 @@ public class RadioManager {
mMetadata = null;
}
mFlags = in.readInt();
- mVendorInfo = readStringMap(in);
+ mVendorInfo = Utils.readStringMap(in);
}
public static final Parcelable.Creator<ProgramInfo> CREATOR
@@ -1592,7 +1574,7 @@ public class RadioManager {
mMetadata.writeToParcel(dest, flags);
}
dest.writeInt(mFlags);
- writeStringMap(dest, mVendorInfo);
+ Utils.writeStringMap(dest, mVendorInfo);
}
@Override
@@ -1727,7 +1709,8 @@ public class RadioManager {
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);
}
@NonNull private final Context mContext;
diff --git a/core/java/android/hardware/radio/RadioTuner.java b/core/java/android/hardware/radio/RadioTuner.java
index 0d367e787122..ed20c4aad761 100644
--- a/core/java/android/hardware/radio/RadioTuner.java
+++ b/core/java/android/hardware/radio/RadioTuner.java
@@ -280,11 +280,29 @@ 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
diff --git a/core/java/android/hardware/radio/TunerAdapter.java b/core/java/android/hardware/radio/TunerAdapter.java
index 8ad609d00816..91944bfd04f0 100644
--- a/core/java/android/hardware/radio/TunerAdapter.java
+++ b/core/java/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();
@@ -227,10 +234,55 @@ class TunerAdapter extends RadioTuner {
@Override
public @NonNull List<RadioManager.ProgramInfo>
getProgramList(@Nullable Map<String, String> vendorFilter) {
- try {
- return mTuner.getProgramList(vendorFilter);
- } catch (RemoteException e) {
- throw new RuntimeException("service died", e);
+ 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;
}
}
diff --git a/core/java/android/hardware/radio/TunerCallbackAdapter.java b/core/java/android/hardware/radio/TunerCallbackAdapter.java
index a01f658e80f6..b299ffe042b2 100644
--- a/core/java/android/hardware/radio/TunerCallbackAdapter.java
+++ b/core/java/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/core/java/android/hardware/radio/Utils.java b/core/java/android/hardware/radio/Utils.java
new file mode 100644
index 000000000000..09bf8feb30c2
--- /dev/null
+++ b/core/java/android/hardware/radio/Utils.java
@@ -0,0 +1,92 @@
+/**
+ * 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 java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+final class 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];
+ }
+ });
+ }
+}