| /** |
| * 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; |
| } |
| } |
| } |