1 /**
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.hardware.radio;
18 
19 import android.annotation.CallbackExecutor;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.SystemApi;
23 import android.os.Parcel;
24 import android.os.Parcelable;
25 
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Objects;
33 import java.util.Set;
34 import java.util.concurrent.Executor;
35 import java.util.stream.Collectors;
36 
37 /**
38  * @hide
39  */
40 @SystemApi
41 public final class ProgramList implements AutoCloseable {
42 
43     private final Object mLock = new Object();
44     private final Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> mPrograms =
45             new HashMap<>();
46 
47     private final List<ListCallback> mListCallbacks = new ArrayList<>();
48     private final List<OnCompleteListener> mOnCompleteListeners = new ArrayList<>();
49     private OnCloseListener mOnCloseListener;
50     private boolean mIsClosed = false;
51     private boolean mIsComplete = false;
52 
ProgramList()53     ProgramList() {}
54 
55     /**
56      * Callback for list change operations.
57      */
58     public abstract static class ListCallback {
59         /**
60          * Called when item was modified or added to the list.
61          */
onItemChanged(@onNull ProgramSelector.Identifier id)62         public void onItemChanged(@NonNull ProgramSelector.Identifier id) { }
63 
64         /**
65          * Called when item was removed from the list.
66          */
onItemRemoved(@onNull ProgramSelector.Identifier id)67         public void onItemRemoved(@NonNull ProgramSelector.Identifier id) { }
68     }
69 
70     /**
71      * Listener of list complete event.
72      */
73     public interface OnCompleteListener {
74         /**
75          * Called when the list turned complete (i.e. when the scan process
76          * came to an end).
77          */
onComplete()78         void onComplete();
79     }
80 
81     interface OnCloseListener {
onClose()82         void onClose();
83     }
84 
85     /**
86      * Registers list change callback with executor.
87      */
registerListCallback(@onNull @allbackExecutor Executor executor, @NonNull ListCallback callback)88     public void registerListCallback(@NonNull @CallbackExecutor Executor executor,
89             @NonNull ListCallback callback) {
90         registerListCallback(new ListCallback() {
91             public void onItemChanged(@NonNull ProgramSelector.Identifier id) {
92                 executor.execute(() -> callback.onItemChanged(id));
93             }
94 
95             public void onItemRemoved(@NonNull ProgramSelector.Identifier id) {
96                 executor.execute(() -> callback.onItemRemoved(id));
97             }
98         });
99     }
100 
101     /**
102      * Registers list change callback.
103      */
registerListCallback(@onNull ListCallback callback)104     public void registerListCallback(@NonNull ListCallback callback) {
105         synchronized (mLock) {
106             if (mIsClosed) return;
107             mListCallbacks.add(Objects.requireNonNull(callback));
108         }
109     }
110 
111     /**
112      * Unregisters list change callback.
113      */
unregisterListCallback(@onNull ListCallback callback)114     public void unregisterListCallback(@NonNull ListCallback callback) {
115         synchronized (mLock) {
116             if (mIsClosed) return;
117             mListCallbacks.remove(Objects.requireNonNull(callback));
118         }
119     }
120 
121     /**
122      * Adds list complete event listener with executor.
123      */
addOnCompleteListener(@onNull @allbackExecutor Executor executor, @NonNull OnCompleteListener listener)124     public void addOnCompleteListener(@NonNull @CallbackExecutor Executor executor,
125             @NonNull OnCompleteListener listener) {
126         addOnCompleteListener(() -> executor.execute(listener::onComplete));
127     }
128 
129     /**
130      * Adds list complete event listener.
131      */
addOnCompleteListener(@onNull OnCompleteListener listener)132     public void addOnCompleteListener(@NonNull OnCompleteListener listener) {
133         synchronized (mLock) {
134             if (mIsClosed) return;
135             mOnCompleteListeners.add(Objects.requireNonNull(listener));
136             if (mIsComplete) listener.onComplete();
137         }
138     }
139 
140     /**
141      * Removes list complete event listener.
142      */
removeOnCompleteListener(@onNull OnCompleteListener listener)143     public void removeOnCompleteListener(@NonNull OnCompleteListener listener) {
144         synchronized (mLock) {
145             if (mIsClosed) return;
146             mOnCompleteListeners.remove(Objects.requireNonNull(listener));
147         }
148     }
149 
setOnCloseListener(@ullable OnCloseListener listener)150     void setOnCloseListener(@Nullable OnCloseListener listener) {
151         synchronized (mLock) {
152             if (mOnCloseListener != null) {
153                 throw new IllegalStateException("Close callback is already set");
154             }
155             mOnCloseListener = listener;
156         }
157     }
158 
159     /**
160      * Disables list updates and releases all resources.
161      */
close()162     public void close() {
163         synchronized (mLock) {
164             if (mIsClosed) return;
165             mIsClosed = true;
166             mPrograms.clear();
167             mListCallbacks.clear();
168             mOnCompleteListeners.clear();
169             if (mOnCloseListener != null) {
170                 mOnCloseListener.onClose();
171                 mOnCloseListener = null;
172             }
173         }
174     }
175 
apply(@onNull Chunk chunk)176     void apply(@NonNull Chunk chunk) {
177         synchronized (mLock) {
178             if (mIsClosed) return;
179 
180             mIsComplete = false;
181 
182             if (chunk.isPurge()) {
183                 new HashSet<>(mPrograms.keySet()).stream().forEach(id -> removeLocked(id));
184             }
185 
186             chunk.getRemoved().stream().forEach(id -> removeLocked(id));
187             chunk.getModified().stream().forEach(info -> putLocked(info));
188 
189             if (chunk.isComplete()) {
190                 mIsComplete = true;
191                 mOnCompleteListeners.forEach(cb -> cb.onComplete());
192             }
193         }
194     }
195 
putLocked(@onNull RadioManager.ProgramInfo value)196     private void putLocked(@NonNull RadioManager.ProgramInfo value) {
197         ProgramSelector.Identifier key = value.getSelector().getPrimaryId();
198         mPrograms.put(Objects.requireNonNull(key), value);
199         ProgramSelector.Identifier sel = value.getSelector().getPrimaryId();
200         mListCallbacks.forEach(cb -> cb.onItemChanged(sel));
201     }
202 
removeLocked(@onNull ProgramSelector.Identifier key)203     private void removeLocked(@NonNull ProgramSelector.Identifier key) {
204         RadioManager.ProgramInfo removed = mPrograms.remove(Objects.requireNonNull(key));
205         if (removed == null) return;
206         ProgramSelector.Identifier sel = removed.getSelector().getPrimaryId();
207         mListCallbacks.forEach(cb -> cb.onItemRemoved(sel));
208     }
209 
210     /**
211      * Converts the program list in its current shape to the static List<>.
212      *
213      * @return the new List<> object; it won't receive any further updates
214      */
toList()215     public @NonNull List<RadioManager.ProgramInfo> toList() {
216         synchronized (mLock) {
217             return mPrograms.values().stream().collect(Collectors.toList());
218         }
219     }
220 
221     /**
222      * Returns the program with a specified primary identifier.
223      *
224      * @param id primary identifier of a program to fetch
225      * @return the program info, or null if there is no such program on the list
226      */
get(@onNull ProgramSelector.Identifier id)227     public @Nullable RadioManager.ProgramInfo get(@NonNull ProgramSelector.Identifier id) {
228         synchronized (mLock) {
229             return mPrograms.get(Objects.requireNonNull(id));
230         }
231     }
232 
233     /**
234      * Filter for the program list.
235      */
236     public static final class Filter implements Parcelable {
237         private final @NonNull Set<Integer> mIdentifierTypes;
238         private final @NonNull Set<ProgramSelector.Identifier> mIdentifiers;
239         private final boolean mIncludeCategories;
240         private final boolean mExcludeModifications;
241         private final @Nullable Map<String, String> mVendorFilter;
242 
243         /**
244          * Constructor of program list filter.
245          *
246          * Arrays passed to this constructor become owned by this object, do not modify them later.
247          *
248          * @param identifierTypes see getIdentifierTypes()
249          * @param identifiers see getIdentifiers()
250          * @param includeCategories see areCategoriesIncluded()
251          * @param excludeModifications see areModificationsExcluded()
252          */
Filter(@onNull Set<Integer> identifierTypes, @NonNull Set<ProgramSelector.Identifier> identifiers, boolean includeCategories, boolean excludeModifications)253         public Filter(@NonNull Set<Integer> identifierTypes,
254                 @NonNull Set<ProgramSelector.Identifier> identifiers,
255                 boolean includeCategories, boolean excludeModifications) {
256             mIdentifierTypes = Objects.requireNonNull(identifierTypes);
257             mIdentifiers = Objects.requireNonNull(identifiers);
258             mIncludeCategories = includeCategories;
259             mExcludeModifications = excludeModifications;
260             mVendorFilter = null;
261         }
262 
263         /**
264          * @hide for framework use only
265          */
Filter()266         public Filter() {
267             mIdentifierTypes = Collections.emptySet();
268             mIdentifiers = Collections.emptySet();
269             mIncludeCategories = false;
270             mExcludeModifications = false;
271             mVendorFilter = null;
272         }
273 
274         /**
275          * @hide for framework use only
276          */
Filter(@ullable Map<String, String> vendorFilter)277         public Filter(@Nullable Map<String, String> vendorFilter) {
278             mIdentifierTypes = Collections.emptySet();
279             mIdentifiers = Collections.emptySet();
280             mIncludeCategories = false;
281             mExcludeModifications = false;
282             mVendorFilter = vendorFilter;
283         }
284 
Filter(@onNull Parcel in)285         private Filter(@NonNull Parcel in) {
286             mIdentifierTypes = Utils.createIntSet(in);
287             mIdentifiers = Utils.createSet(in, ProgramSelector.Identifier.CREATOR);
288             mIncludeCategories = in.readByte() != 0;
289             mExcludeModifications = in.readByte() != 0;
290             mVendorFilter = Utils.readStringMap(in);
291         }
292 
293         @Override
writeToParcel(Parcel dest, int flags)294         public void writeToParcel(Parcel dest, int flags) {
295             Utils.writeIntSet(dest, mIdentifierTypes);
296             Utils.writeSet(dest, mIdentifiers);
297             dest.writeByte((byte) (mIncludeCategories ? 1 : 0));
298             dest.writeByte((byte) (mExcludeModifications ? 1 : 0));
299             Utils.writeStringMap(dest, mVendorFilter);
300         }
301 
302         @Override
describeContents()303         public int describeContents() {
304             return 0;
305         }
306 
307         public static final @android.annotation.NonNull Parcelable.Creator<Filter> CREATOR = new Parcelable.Creator<Filter>() {
308             public Filter createFromParcel(Parcel in) {
309                 return new Filter(in);
310             }
311 
312             public Filter[] newArray(int size) {
313                 return new Filter[size];
314             }
315         };
316 
317         /**
318          * @hide for framework use only
319          */
getVendorFilter()320         public Map<String, String> getVendorFilter() {
321             return mVendorFilter;
322         }
323 
324         /**
325          * Returns the list of identifier types that satisfy the filter.
326          *
327          * If the program list entry contains at least one identifier of the type
328          * listed, it satisfies this condition.
329          *
330          * Empty list means no filtering on identifier type.
331          *
332          * @return the list of accepted identifier types, must not be modified
333          */
getIdentifierTypes()334         public @NonNull Set<Integer> getIdentifierTypes() {
335             return mIdentifierTypes;
336         }
337 
338         /**
339          * Returns the list of identifiers that satisfy the filter.
340          *
341          * If the program list entry contains at least one listed identifier,
342          * it satisfies this condition.
343          *
344          * Empty list means no filtering on identifier.
345          *
346          * @return the list of accepted identifiers, must not be modified
347          */
getIdentifiers()348         public @NonNull Set<ProgramSelector.Identifier> getIdentifiers() {
349             return mIdentifiers;
350         }
351 
352         /**
353          * Checks, if non-tunable entries that define tree structure on the
354          * program list (i.e. DAB ensembles) should be included.
355          */
areCategoriesIncluded()356         public boolean areCategoriesIncluded() {
357             return mIncludeCategories;
358         }
359 
360         /**
361          * Checks, if updates on entry modifications should be disabled.
362          *
363          * If true, 'modified' vector of ProgramListChunk must contain list
364          * additions only. Once the program is added to the list, it's not
365          * updated anymore.
366          */
areModificationsExcluded()367         public boolean areModificationsExcluded() {
368             return mExcludeModifications;
369         }
370     }
371 
372     /**
373      * @hide This is a transport class used for internal communication between
374      *       Broadcast Radio Service and RadioManager.
375      *       Do not use it directly.
376      */
377     public static final class Chunk implements Parcelable {
378         private final boolean mPurge;
379         private final boolean mComplete;
380         private final @NonNull Set<RadioManager.ProgramInfo> mModified;
381         private final @NonNull Set<ProgramSelector.Identifier> mRemoved;
382 
Chunk(boolean purge, boolean complete, @Nullable Set<RadioManager.ProgramInfo> modified, @Nullable Set<ProgramSelector.Identifier> removed)383         public Chunk(boolean purge, boolean complete,
384                 @Nullable Set<RadioManager.ProgramInfo> modified,
385                 @Nullable Set<ProgramSelector.Identifier> removed) {
386             mPurge = purge;
387             mComplete = complete;
388             mModified = (modified != null) ? modified : Collections.emptySet();
389             mRemoved = (removed != null) ? removed : Collections.emptySet();
390         }
391 
Chunk(@onNull Parcel in)392         private Chunk(@NonNull Parcel in) {
393             mPurge = in.readByte() != 0;
394             mComplete = in.readByte() != 0;
395             mModified = Utils.createSet(in, RadioManager.ProgramInfo.CREATOR);
396             mRemoved = Utils.createSet(in, ProgramSelector.Identifier.CREATOR);
397         }
398 
399         @Override
writeToParcel(Parcel dest, int flags)400         public void writeToParcel(Parcel dest, int flags) {
401             dest.writeByte((byte) (mPurge ? 1 : 0));
402             dest.writeByte((byte) (mComplete ? 1 : 0));
403             Utils.writeSet(dest, mModified);
404             Utils.writeSet(dest, mRemoved);
405         }
406 
407         @Override
describeContents()408         public int describeContents() {
409             return 0;
410         }
411 
412         public static final @android.annotation.NonNull Parcelable.Creator<Chunk> CREATOR = new Parcelable.Creator<Chunk>() {
413             public Chunk createFromParcel(Parcel in) {
414                 return new Chunk(in);
415             }
416 
417             public Chunk[] newArray(int size) {
418                 return new Chunk[size];
419             }
420         };
421 
isPurge()422         public boolean isPurge() {
423             return mPurge;
424         }
425 
isComplete()426         public boolean isComplete() {
427             return mComplete;
428         }
429 
getModified()430         public @NonNull Set<RadioManager.ProgramInfo> getModified() {
431             return mModified;
432         }
433 
getRemoved()434         public @NonNull Set<ProgramSelector.Identifier> getRemoved() {
435             return mRemoved;
436         }
437     }
438 }
439