From 436128f23ae4efc52bbfa3cdde666a139b4bedc4 Mon Sep 17 00:00:00 2001 From: Tomasz Wasilczyk Date: Mon, 8 Jan 2018 16:46:09 -0800 Subject: [PATCH] Implement front-end APIs for dynamic program list. Bug: 69860743 Test: instrumentation Change-Id: I326865c690d315b867626599174e34911564ef9e --- api/system-current.txt | 38 +- core/java/android/hardware/radio/ITuner.aidl | 11 +- .../hardware/radio/ITunerCallback.aidl | 2 + .../android/hardware/radio/ProgramList.aidl | 23 + .../android/hardware/radio/ProgramList.java | 427 ++++++++++++++++++ .../hardware/radio/ProgramSelector.java | 6 +- .../android/hardware/radio/RadioManager.java | 35 +- .../android/hardware/radio/RadioTuner.java | 18 + .../android/hardware/radio/TunerAdapter.java | 70 ++- .../hardware/radio/TunerCallbackAdapter.java | 73 ++- core/java/android/hardware/radio/Utils.java | 92 ++++ .../broadcastradio/BroadcastRadioService.java | 3 +- .../server/broadcastradio/hal1/Tuner.java | 14 +- .../broadcastradio/hal1/TunerCallback.java | 38 ++ .../hal2/BroadcastRadioService.java | 2 +- .../server/broadcastradio/hal2/Convert.java | 139 ++++-- .../broadcastradio/hal2/RadioModule.java | 16 +- .../broadcastradio/hal2/TunerCallback.java | 4 +- .../broadcastradio/hal2/TunerSession.java | 23 +- 19 files changed, 932 insertions(+), 102 deletions(-) create mode 100644 core/java/android/hardware/radio/ProgramList.aidl create mode 100644 core/java/android/hardware/radio/ProgramList.java create mode 100644 core/java/android/hardware/radio/Utils.java diff --git a/api/system-current.txt b/api/system-current.txt index b47304a41bc2a..d68b6afb00012 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -1701,6 +1701,39 @@ package android.hardware.location { package android.hardware.radio { + public final class ProgramList implements java.lang.AutoCloseable { + method public void addOnCompleteListener(java.util.concurrent.Executor, android.hardware.radio.ProgramList.OnCompleteListener); + method public void addOnCompleteListener(android.hardware.radio.ProgramList.OnCompleteListener); + method public void close(); + method public android.hardware.radio.RadioManager.ProgramInfo get(android.hardware.radio.ProgramSelector.Identifier); + method public void registerListCallback(java.util.concurrent.Executor, android.hardware.radio.ProgramList.ListCallback); + method public void registerListCallback(android.hardware.radio.ProgramList.ListCallback); + method public void removeOnCompleteListener(android.hardware.radio.ProgramList.OnCompleteListener); + method public java.util.List toList(); + method public void unregisterListCallback(android.hardware.radio.ProgramList.ListCallback); + } + + public static final class ProgramList.Filter implements android.os.Parcelable { + ctor public ProgramList.Filter(java.util.Set, java.util.Set, boolean, boolean); + method public boolean areCategoriesIncluded(); + method public boolean areModificationsExcluded(); + method public int describeContents(); + method public java.util.Set getIdentifierTypes(); + method public java.util.Set getIdentifiers(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static abstract class ProgramList.ListCallback { + ctor public ProgramList.ListCallback(); + method public void onItemChanged(android.hardware.radio.ProgramSelector.Identifier); + method public void onItemRemoved(android.hardware.radio.ProgramSelector.Identifier); + } + + public static abstract interface ProgramList.OnCompleteListener { + method public abstract void onComplete(); + } + public final class ProgramSelector implements android.os.Parcelable { ctor public ProgramSelector(int, android.hardware.radio.ProgramSelector.Identifier, android.hardware.radio.ProgramSelector.Identifier[], long[]); method public static android.hardware.radio.ProgramSelector createAmFmSelector(int, int); @@ -1724,6 +1757,7 @@ package android.hardware.radio { field public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9; // 0x9 field public static final int IDENTIFIER_TYPE_HD_STATION_ID_EXT = 3; // 0x3 field public static final int IDENTIFIER_TYPE_HD_SUBCHANNEL = 4; // 0x4 + field public static final int IDENTIFIER_TYPE_INVALID = 0; // 0x0 field public static final int IDENTIFIER_TYPE_RDS_PI = 2; // 0x2 field public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13; // 0xd field public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12; // 0xc @@ -1735,6 +1769,7 @@ package android.hardware.radio { field public static final int PROGRAM_TYPE_DRMO = 6; // 0x6 field public static final int PROGRAM_TYPE_FM = 2; // 0x2 field public static final int PROGRAM_TYPE_FM_HD = 4; // 0x4 + field public static final int PROGRAM_TYPE_INVALID = 0; // 0x0 field public static final int PROGRAM_TYPE_SXM = 7; // 0x7 field public static final int PROGRAM_TYPE_VENDOR_END = 1999; // 0x7cf field public static final int PROGRAM_TYPE_VENDOR_START = 1000; // 0x3e8 @@ -1953,10 +1988,11 @@ package android.hardware.radio { method public abstract void cancelAnnouncement(); method public abstract void close(); method public abstract int getConfiguration(android.hardware.radio.RadioManager.BandConfig[]); + method public android.hardware.radio.ProgramList getDynamicProgramList(android.hardware.radio.ProgramList.Filter); method public abstract boolean getMute(); method public java.util.Map getParameters(java.util.List); method public abstract int getProgramInformation(android.hardware.radio.RadioManager.ProgramInfo[]); - method public abstract java.util.List getProgramList(java.util.Map); + method public abstract deprecated java.util.List getProgramList(java.util.Map); method public abstract boolean hasControl(); method public abstract deprecated boolean isAnalogForced(); method public abstract boolean isAntennaConnected(); diff --git a/core/java/android/hardware/radio/ITuner.aidl b/core/java/android/hardware/radio/ITuner.aidl index ca380769954bc..bf5e391794f55 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 - * @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 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 775e25c7e7cf9..54af30fcc35eb 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 diff --git a/core/java/android/hardware/radio/ProgramList.aidl b/core/java/android/hardware/radio/ProgramList.aidl new file mode 100644 index 0000000000000..34b7f97558c74 --- /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 0000000000000..b2aa9ba532a95 --- /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 mPrograms = + new HashMap<>(); + + private final List mListCallbacks = new ArrayList<>(); + private final List 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 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 mIdentifierTypes; + private final @NonNull Set mIdentifiers; + private final boolean mIncludeCategories; + private final boolean mExcludeModifications; + private final @Nullable Map 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 identifierTypes, + @NonNull Set 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 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 CREATOR = new Parcelable.Creator() { + 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 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 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 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 mModified; + private final @NonNull Set mRemoved; + + public Chunk(boolean purge, boolean complete, + @Nullable Set modified, + @Nullable Set 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 CREATOR = new Parcelable.Creator() { + 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 getModified() { + return mModified; + } + + public @NonNull Set getRemoved() { + return mRemoved; + } + } +} diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java index 2211cee9b3151..3556751f4af4e 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 b740f1430157f..56668ac00527e 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 map) { - dest.writeInt(map.size()); - for (Map.Entry entry : map.entrySet()) { - dest.writeString(entry.getKey()); - dest.writeString(entry.getValue()); - } - } - - private static @NonNull Map readStringMap(@NonNull Parcel in) { - int size = in.readInt(); - Map 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 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 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 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 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 0d367e7871222..ed20c4aad761e 100644 --- a/core/java/android/hardware/radio/RadioTuner.java +++ b/core/java/android/hardware/radio/RadioTuner.java @@ -280,10 +280,28 @@ 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 getProgramList(@Nullable Map 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. * diff --git a/core/java/android/hardware/radio/TunerAdapter.java b/core/java/android/hardware/radio/TunerAdapter.java index 8ad609d008166..91944bfd04f0e 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 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 getProgramList(@Nullable Map 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 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 a01f658e80f6a..b299ffe042b25 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 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 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 @@ -97,6 +160,14 @@ class TunerCallbackAdapter extends ITunerCallback.Stub { mHandler.post(() -> mCallback.onProgramListChanged()); } + @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 0000000000000..09bf8feb30c21 --- /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 map) { + if (map == null) { + dest.writeInt(0); + return; + } + dest.writeInt(map.size()); + for (Map.Entry entry : map.entrySet()) { + dest.writeString(entry.getKey()); + dest.writeString(entry.getValue()); + } + } + + static @NonNull Map readStringMap(@NonNull Parcel in) { + int size = in.readInt(); + Map map = new HashMap<>(); + while (size-- > 0) { + String key = in.readString(); + String value = in.readString(); + map.put(key, value); + } + return map; + } + + static void writeSet(@NonNull Parcel dest, @Nullable Set set) { + if (set == null) { + dest.writeInt(0); + return; + } + dest.writeInt(set.size()); + set.stream().forEach(elem -> dest.writeTypedObject(elem, 0)); + } + + static Set createSet(@NonNull Parcel in, Parcelable.Creator c) { + int size = in.readInt(); + Set set = new HashSet<>(); + while (size-- > 0) { + set.add(in.readTypedObject(c)); + } + return set; + } + + static void writeIntSet(@NonNull Parcel dest, @Nullable Set set) { + if (set == null) { + dest.writeInt(0); + return; + } + dest.writeInt(set.size()); + set.stream().forEach(elem -> dest.writeInt(Objects.requireNonNull(elem))); + } + + static Set createIntSet(@NonNull Parcel in) { + return createSet(in, new Parcelable.Creator() { + public Integer createFromParcel(Parcel in) { + return in.readInt(); + } + + public Integer[] newArray(int size) { + return new Integer[size]; + } + }); + } +} diff --git a/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java index 7d3b670bdfa3a..10e6cad70bcdc 100644 --- a/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java +++ b/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java @@ -25,6 +25,7 @@ import android.hardware.radio.ITuner; import android.hardware.radio.ITunerCallback; import android.hardware.radio.RadioManager; import android.os.ParcelableException; +import android.os.RemoteException; import android.util.Slog; import com.android.server.SystemService; @@ -86,7 +87,7 @@ public class BroadcastRadioService extends SystemService { @Override public ITuner openTuner(int moduleId, RadioManager.BandConfig bandConfig, - boolean withAudio, ITunerCallback callback) { + boolean withAudio, ITunerCallback callback) throws RemoteException { Slog.i(TAG, "openTuner(" + moduleId + ", _, " + withAudio + ", _)"); enforcePolicyAccess(); if (callback == null) { diff --git a/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java b/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java index e5090ed5b715b..f9b35f53d4d54 100644 --- a/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java +++ b/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java @@ -21,6 +21,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.hardware.radio.ITuner; import android.hardware.radio.ITunerCallback; +import android.hardware.radio.ProgramList; import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; import android.os.IBinder; @@ -249,8 +250,7 @@ class Tuner extends ITuner.Stub { } } - @Override - public List getProgramList(Map vendorFilter) { + List getProgramList(Map vendorFilter) { Map sFilter = vendorFilter; synchronized (mLock) { checkNotClosedLocked(); @@ -262,6 +262,16 @@ class Tuner extends ITuner.Stub { } } + @Override + public void startProgramListUpdates(ProgramList.Filter filter) { + mTunerCallback.startProgramListUpdates(filter); + } + + @Override + public void stopProgramListUpdates() { + mTunerCallback.stopProgramListUpdates(); + } + @Override public boolean isConfigFlagSupported(int flag) { return flag == RadioManager.CONFIG_FORCE_ANALOG; diff --git a/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java b/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java index 673ff88d5c981..18f56ed584750 100644 --- a/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java +++ b/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java @@ -19,6 +19,7 @@ package com.android.server.broadcastradio.hal1; import android.annotation.NonNull; import android.hardware.radio.ITuner; import android.hardware.radio.ITunerCallback; +import android.hardware.radio.ProgramList; import android.hardware.radio.RadioManager; import android.hardware.radio.RadioMetadata; import android.hardware.radio.RadioTuner; @@ -28,6 +29,10 @@ import android.util.Slog; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; class TunerCallback implements ITunerCallback { private static final String TAG = "BroadcastRadioService.TunerCallback"; @@ -40,6 +45,8 @@ class TunerCallback implements ITunerCallback { @NonNull private final Tuner mTuner; @NonNull private final ITunerCallback mClientCallback; + private final AtomicReference mProgramListFilter = new AtomicReference<>(); + TunerCallback(@NonNull Tuner tuner, @NonNull ITunerCallback clientCallback, int halRev) { mTuner = tuner; mClientCallback = clientCallback; @@ -78,6 +85,15 @@ class TunerCallback implements ITunerCallback { mTuner.close(); } + void startProgramListUpdates(@NonNull ProgramList.Filter filter) { + mProgramListFilter.set(Objects.requireNonNull(filter)); + sendProgramListUpdate(); + } + + void stopProgramListUpdates() { + mProgramListFilter.set(null); + } + @Override public void onError(int status) { dispatch(() -> mClientCallback.onError(status)); @@ -121,6 +137,28 @@ class TunerCallback implements ITunerCallback { @Override public void onProgramListChanged() { dispatch(() -> mClientCallback.onProgramListChanged()); + sendProgramListUpdate(); + } + + private void sendProgramListUpdate() { + ProgramList.Filter filter = mProgramListFilter.get(); + if (filter == null) return; + + List modified; + try { + modified = mTuner.getProgramList(filter.getVendorFilter()); + } catch (IllegalStateException ex) { + Slog.d(TAG, "Program list not ready yet"); + return; + } + Set modifiedSet = modified.stream().collect(Collectors.toSet()); + ProgramList.Chunk chunk = new ProgramList.Chunk(true, true, modifiedSet, null); + dispatch(() -> mClientCallback.onProgramListUpdated(chunk)); + } + + @Override + public void onProgramListUpdated(ProgramList.Chunk chunk) { + dispatch(() -> mClientCallback.onProgramListUpdated(chunk)); } @Override diff --git a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java index 9158ff0cb66f0..fc9a5d61704db 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java @@ -82,7 +82,7 @@ public class BroadcastRadioService { } public ITuner openSession(int moduleId, @Nullable RadioManager.BandConfig legacyConfig, - boolean withAudio, @NonNull ITunerCallback callback) { + boolean withAudio, @NonNull ITunerCallback callback) throws RemoteException { Objects.requireNonNull(callback); if (!withAudio) { diff --git a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java index 2c129bbac33e6..60a927c569591 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java @@ -20,15 +20,22 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.hardware.broadcastradio.V2_0.AmFmBandRange; import android.hardware.broadcastradio.V2_0.AmFmRegionConfig; +import android.hardware.broadcastradio.V2_0.ProgramFilter; +import android.hardware.broadcastradio.V2_0.ProgramIdentifier; +import android.hardware.broadcastradio.V2_0.ProgramInfo; +import android.hardware.broadcastradio.V2_0.ProgramInfoFlags; +import android.hardware.broadcastradio.V2_0.ProgramListChunk; import android.hardware.broadcastradio.V2_0.Properties; import android.hardware.broadcastradio.V2_0.Result; import android.hardware.broadcastradio.V2_0.VendorKeyValue; +import android.hardware.radio.ProgramList; import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; import android.os.ParcelableException; import android.util.Slog; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -36,6 +43,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; class Convert { private static final String TAG = "BcRadio2Srv.convert"; @@ -78,43 +86,52 @@ class Convert { return map; } + private static @ProgramSelector.ProgramType int identifierTypeToProgramType( + @ProgramSelector.IdentifierType int idType) { + switch (idType) { + case ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY: + case ProgramSelector.IDENTIFIER_TYPE_RDS_PI: + // TODO(b/69958423): verify AM/FM with frequency range + return ProgramSelector.PROGRAM_TYPE_FM; + case ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT: + // TODO(b/69958423): verify AM/FM with frequency range + return ProgramSelector.PROGRAM_TYPE_FM_HD; + case ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC: + case ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE: + case ProgramSelector.IDENTIFIER_TYPE_DAB_SCID: + case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY: + return ProgramSelector.PROGRAM_TYPE_DAB; + case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID: + case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY: + return ProgramSelector.PROGRAM_TYPE_DRMO; + case ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID: + case ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL: + return ProgramSelector.PROGRAM_TYPE_SXM; + } + if (idType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_START + && idType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_END) { + return idType; + } + return ProgramSelector.PROGRAM_TYPE_INVALID; + } + private static @NonNull int[] identifierTypesToProgramTypes(@NonNull int[] idTypes) { Set pTypes = new HashSet<>(); for (int idType : idTypes) { - switch (idType) { - case ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY: - case ProgramSelector.IDENTIFIER_TYPE_RDS_PI: - // TODO(b/69958423): verify AM/FM with region info - pTypes.add(ProgramSelector.PROGRAM_TYPE_AM); - pTypes.add(ProgramSelector.PROGRAM_TYPE_FM); - break; - case ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT: - // TODO(b/69958423): verify AM/FM with region info - pTypes.add(ProgramSelector.PROGRAM_TYPE_AM_HD); - pTypes.add(ProgramSelector.PROGRAM_TYPE_FM_HD); - break; - case ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC: - case ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE: - case ProgramSelector.IDENTIFIER_TYPE_DAB_SCID: - case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY: - pTypes.add(ProgramSelector.PROGRAM_TYPE_DAB); - break; - case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID: - case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY: - pTypes.add(ProgramSelector.PROGRAM_TYPE_DRMO); - break; - case ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID: - case ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL: - pTypes.add(ProgramSelector.PROGRAM_TYPE_SXM); - break; - default: - break; + int pType = identifierTypeToProgramType(idType); + + if (pType == ProgramSelector.PROGRAM_TYPE_INVALID) continue; + + pTypes.add(pType); + if (pType == ProgramSelector.PROGRAM_TYPE_FM) { + // TODO(b/69958423): verify AM/FM with region info + pTypes.add(ProgramSelector.PROGRAM_TYPE_AM); } - if (idType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_START - && idType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_END) { - pTypes.add(idType); + if (pType == ProgramSelector.PROGRAM_TYPE_FM_HD) { + // TODO(b/69958423): verify AM/FM with region info + pTypes.add(ProgramSelector.PROGRAM_TYPE_AM_HD); } } @@ -189,6 +206,64 @@ class Convert { false, // isBgScanSupported is deprecated supportedProgramTypes, supportedIdentifierTypes, - vendorInfoFromHal(prop.vendorInfo)); + vendorInfoFromHal(prop.vendorInfo) + ); + } + + static @NonNull ProgramIdentifier programIdentifierToHal( + @NonNull ProgramSelector.Identifier id) { + ProgramIdentifier hwId = new ProgramIdentifier(); + hwId.type = id.getType(); + hwId.value = id.getValue(); + return hwId; + } + + static @NonNull ProgramSelector.Identifier programIdentifierFromHal(@NonNull ProgramIdentifier id) { + return new ProgramSelector.Identifier(id.type, id.value); + } + + static @NonNull ProgramSelector programSelectorFromHal( + @NonNull android.hardware.broadcastradio.V2_0.ProgramSelector sel) { + ProgramSelector.Identifier[] secondaryIds = sel.secondaryIds.stream().map( + id -> programIdentifierFromHal(id)).toArray(ProgramSelector.Identifier[]::new); + + return new ProgramSelector( + identifierTypeToProgramType(sel.primaryId.type), + programIdentifierFromHal(sel.primaryId), + secondaryIds, null); + } + + static @NonNull RadioManager.ProgramInfo programInfoFromHal(@NonNull ProgramInfo info) { + return new RadioManager.ProgramInfo( + programSelectorFromHal(info.selector), + (info.infoFlags & ProgramInfoFlags.TUNED) != 0, + (info.infoFlags & ProgramInfoFlags.STEREO) != 0, + false, // TODO(b/69860743): digital + info.signalQuality, + null, // TODO(b/69860743): metadata + info.infoFlags, + vendorInfoFromHal(info.vendorInfo) + ); + } + + static @NonNull ProgramFilter programFilterToHal(@NonNull ProgramList.Filter filter) { + ProgramFilter hwFilter = new ProgramFilter(); + + filter.getIdentifierTypes().stream().forEachOrdered(hwFilter.identifierTypes::add); + filter.getIdentifiers().stream().forEachOrdered( + id -> hwFilter.identifiers.add(programIdentifierToHal(id))); + hwFilter.includeCategories = filter.areCategoriesIncluded(); + hwFilter.excludeModifications = filter.areModificationsExcluded(); + + return hwFilter; + } + + static @NonNull ProgramList.Chunk programListChunkFromHal(@NonNull ProgramListChunk chunk) { + Set modified = chunk.modified.stream().map( + info -> programInfoFromHal(info)).collect(Collectors.toSet()); + Set removed = chunk.removed.stream().map( + id -> programIdentifierFromHal(id)).collect(Collectors.toSet()); + + return new ProgramList.Chunk(chunk.purge, chunk.complete, modified, removed); } } diff --git a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java index 45b21900ff332..c8e15c1a2f4fe 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java @@ -63,20 +63,16 @@ class RadioModule { } } - public @NonNull TunerSession openSession(@NonNull android.hardware.radio.ITunerCallback userCb) { + public @NonNull TunerSession openSession(@NonNull android.hardware.radio.ITunerCallback userCb) + throws RemoteException { TunerCallback cb = new TunerCallback(Objects.requireNonNull(userCb)); Mutable hwSession = new Mutable<>(); MutableInt halResult = new MutableInt(Result.UNKNOWN_ERROR); - try { - mService.openSession(cb, (int result, ITunerSession session) -> { - hwSession.value = session; - halResult.value = result; - }); - } catch (RemoteException ex) { - Slog.e(TAG, "failed to open session", ex); - throw new ParcelableException(ex); - } + mService.openSession(cb, (int result, ITunerSession session) -> { + hwSession.value = session; + halResult.value = result; + }); Convert.throwOnError("openSession", halResult.value); Objects.requireNonNull(hwSession.value); diff --git a/services/core/java/com/android/server/broadcastradio/hal2/TunerCallback.java b/services/core/java/com/android/server/broadcastradio/hal2/TunerCallback.java index c9084ee9517ac..ed2a1b3ce8518 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/TunerCallback.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/TunerCallback.java @@ -56,7 +56,9 @@ class TunerCallback extends ITunerCallback.Stub { public void onCurrentProgramInfoChanged(ProgramInfo info) {} @Override - public void onProgramListUpdated(ProgramListChunk chunk) {} + public void onProgramListUpdated(ProgramListChunk chunk) { + dispatch(() -> mClientCb.onProgramListUpdated(Convert.programListChunkFromHal(chunk))); + } @Override public void onAntennaStateChange(boolean connected) {} diff --git a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java index 8ed646af2b888..e093c9df7c4b7 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java @@ -22,6 +22,7 @@ import android.hardware.broadcastradio.V2_0.ConfigFlag; import android.hardware.broadcastradio.V2_0.ITunerSession; import android.hardware.broadcastradio.V2_0.Result; import android.hardware.radio.ITuner; +import android.hardware.radio.ProgramList; import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; import android.media.AudioSystem; @@ -184,10 +185,19 @@ class TunerSession extends ITuner.Stub { } @Override - public List getProgramList(Map vendorFilter) { + public void startProgramListUpdates(ProgramList.Filter filter) throws RemoteException { synchronized (mLock) { checkNotClosedLocked(); - return null; + int halResult = mHwSession.startProgramListUpdates(Convert.programFilterToHal(filter)); + Convert.throwOnError("startProgramListUpdates", halResult); + } + } + + @Override + public void stopProgramListUpdates() throws RemoteException { + synchronized (mLock) { + checkNotClosedLocked(); + mHwSession.stopProgramListUpdates(); } } @@ -226,17 +236,12 @@ class TunerSession extends ITuner.Stub { } @Override - public void setConfigFlag(int flag, boolean value) { + public void setConfigFlag(int flag, boolean value) throws RemoteException { Slog.v(TAG, "setConfigFlag " + ConfigFlag.toString(flag) + " = " + value); synchronized (mLock) { checkNotClosedLocked(); - int halResult; - try { - halResult = mHwSession.setConfigFlag(flag, value); - } catch (RemoteException ex) { - throw new RuntimeException("Failed to set flag " + ConfigFlag.toString(flag), ex); - } + int halResult = mHwSession.setConfigFlag(flag, value); Convert.throwOnError("setConfigFlag", halResult); } }