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          *
356          * @see {@link ProgramSelector.Identifier#isCategory()}
357          */
areCategoriesIncluded()358         public boolean areCategoriesIncluded() {
359             return mIncludeCategories;
360         }
361 
362         /**
363          * Checks, if updates on entry modifications should be disabled.
364          *
365          * If true, 'modified' vector of ProgramListChunk must contain list
366          * additions only. Once the program is added to the list, it's not
367          * updated anymore.
368          */
areModificationsExcluded()369         public boolean areModificationsExcluded() {
370             return mExcludeModifications;
371         }
372 
373         @Override
hashCode()374         public int hashCode() {
375             return Objects.hash(mIdentifierTypes, mIdentifiers, mIncludeCategories,
376                     mExcludeModifications);
377         }
378 
379         @Override
equals(@ullable Object obj)380         public boolean equals(@Nullable Object obj) {
381             if (this == obj) return true;
382             if (!(obj instanceof Filter)) return false;
383             Filter other = (Filter) obj;
384 
385             if (mIncludeCategories != other.mIncludeCategories) return false;
386             if (mExcludeModifications != other.mExcludeModifications) return false;
387             if (!Objects.equals(mIdentifierTypes, other.mIdentifierTypes)) return false;
388             if (!Objects.equals(mIdentifiers, other.mIdentifiers)) return false;
389             return true;
390         }
391 
392         @NonNull
393         @Override
toString()394         public String toString() {
395             return "Filter [mIdentifierTypes=" + mIdentifierTypes
396                     + ", mIdentifiers=" + mIdentifiers
397                     + ", mIncludeCategories=" + mIncludeCategories
398                     + ", mExcludeModifications=" + mExcludeModifications + "]";
399         }
400     }
401 
402     /**
403      * @hide This is a transport class used for internal communication between
404      *       Broadcast Radio Service and RadioManager.
405      *       Do not use it directly.
406      */
407     public static final class Chunk implements Parcelable {
408         private final boolean mPurge;
409         private final boolean mComplete;
410         private final @NonNull Set<RadioManager.ProgramInfo> mModified;
411         private final @NonNull Set<ProgramSelector.Identifier> mRemoved;
412 
Chunk(boolean purge, boolean complete, @Nullable Set<RadioManager.ProgramInfo> modified, @Nullable Set<ProgramSelector.Identifier> removed)413         public Chunk(boolean purge, boolean complete,
414                 @Nullable Set<RadioManager.ProgramInfo> modified,
415                 @Nullable Set<ProgramSelector.Identifier> removed) {
416             mPurge = purge;
417             mComplete = complete;
418             mModified = (modified != null) ? modified : Collections.emptySet();
419             mRemoved = (removed != null) ? removed : Collections.emptySet();
420         }
421 
Chunk(@onNull Parcel in)422         private Chunk(@NonNull Parcel in) {
423             mPurge = in.readByte() != 0;
424             mComplete = in.readByte() != 0;
425             mModified = Utils.createSet(in, RadioManager.ProgramInfo.CREATOR);
426             mRemoved = Utils.createSet(in, ProgramSelector.Identifier.CREATOR);
427         }
428 
429         @Override
writeToParcel(Parcel dest, int flags)430         public void writeToParcel(Parcel dest, int flags) {
431             dest.writeByte((byte) (mPurge ? 1 : 0));
432             dest.writeByte((byte) (mComplete ? 1 : 0));
433             Utils.writeSet(dest, mModified);
434             Utils.writeSet(dest, mRemoved);
435         }
436 
437         @Override
describeContents()438         public int describeContents() {
439             return 0;
440         }
441 
442         public static final @android.annotation.NonNull Parcelable.Creator<Chunk> CREATOR = new Parcelable.Creator<Chunk>() {
443             public Chunk createFromParcel(Parcel in) {
444                 return new Chunk(in);
445             }
446 
447             public Chunk[] newArray(int size) {
448                 return new Chunk[size];
449             }
450         };
451 
isPurge()452         public boolean isPurge() {
453             return mPurge;
454         }
455 
isComplete()456         public boolean isComplete() {
457             return mComplete;
458         }
459 
getModified()460         public @NonNull Set<RadioManager.ProgramInfo> getModified() {
461             return mModified;
462         }
463 
getRemoved()464         public @NonNull Set<ProgramSelector.Identifier> getRemoved() {
465             return mRemoved;
466         }
467 
468         @Override
equals(Object obj)469         public boolean equals(Object obj) {
470             if (this == obj) return true;
471             if (!(obj instanceof Chunk)) return false;
472             Chunk other = (Chunk) obj;
473 
474             if (mPurge != other.mPurge) return false;
475             if (mComplete != other.mComplete) return false;
476             if (!Objects.equals(mModified, other.mModified)) return false;
477             if (!Objects.equals(mRemoved, other.mRemoved)) return false;
478             return true;
479         }
480 
481         @Override
toString()482         public String toString() {
483             return "Chunk [mPurge=" + mPurge + ", mComplete=" + mComplete
484                     + ", mModified=" + mModified + ", mRemoved=" + mRemoved + "]";
485         }
486     }
487 }
488