/** * 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() { mIdentifierTypes = Collections.emptySet(); mIdentifiers = Collections.emptySet(); mIncludeCategories = false; mExcludeModifications = false; 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; } } }