blob: 69f927375dd9b9ffdee6ab52cefc57d11617bc08 [file] [log] [blame]
/**
* 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() {
mIdentifierTypes = Collections.emptySet();
mIdentifiers = Collections.emptySet();
mIncludeCategories = false;
mExcludeModifications = false;
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 @android.annotation.NonNull 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 @android.annotation.NonNull 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;
}
}
}