Implement front-end APIs for dynamic program list.

Bug: 69860743
Test: instrumentation
Change-Id: I326865c690d315b867626599174e34911564ef9e
This commit is contained in:
Tomasz Wasilczyk
2018-01-08 16:46:09 -08:00
parent 4d5420f066
commit 436128f23a
19 changed files with 932 additions and 102 deletions

View File

@@ -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);

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<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.
*

View File

@@ -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;
}
}

View File

@@ -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
@@ -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));

View File

@@ -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];
}
});
}
}