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 import android.util.ArrayMap;
26 
27 import com.android.internal.annotations.GuardedBy;
28 
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.Iterator;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Objects;
35 import java.util.Set;
36 import java.util.concurrent.Executor;
37 import java.util.stream.Collectors;
38 
39 /**
40  * @hide
41  */
42 @SystemApi
43 public final class ProgramList implements AutoCloseable {
44 
45     private final Object mLock = new Object();
46 
47     @GuardedBy("mLock")
48     private final Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> mPrograms =
49             new ArrayMap<>();
50 
51     @GuardedBy("mLock")
52     private final List<ListCallback> mListCallbacks = new ArrayList<>();
53 
54     @GuardedBy("mLock")
55     private final List<OnCompleteListener> mOnCompleteListeners = new ArrayList<>();
56 
57     @GuardedBy("mLock")
58     private OnCloseListener mOnCloseListener;
59 
60     @GuardedBy("mLock")
61     private boolean mIsClosed;
62 
63     @GuardedBy("mLock")
64     private boolean mIsComplete;
65 
ProgramList()66     ProgramList() {}
67 
68     /**
69      * Callback for list change operations.
70      */
71     public abstract static class ListCallback {
72         /**
73          * Called when item was modified or added to the list.
74          */
onItemChanged(@onNull ProgramSelector.Identifier id)75         public void onItemChanged(@NonNull ProgramSelector.Identifier id) { }
76 
77         /**
78          * Called when item was removed from the list.
79          */
onItemRemoved(@onNull ProgramSelector.Identifier id)80         public void onItemRemoved(@NonNull ProgramSelector.Identifier id) { }
81     }
82 
83     /**
84      * Listener of list complete event.
85      */
86     public interface OnCompleteListener {
87         /**
88          * Called when the list turned complete (i.e. when the scan process
89          * came to an end).
90          */
onComplete()91         void onComplete();
92     }
93 
94     interface OnCloseListener {
onClose()95         void onClose();
96     }
97 
98     /**
99      * Registers list change callback with executor.
100      */
registerListCallback(@onNull @allbackExecutor Executor executor, @NonNull ListCallback callback)101     public void registerListCallback(@NonNull @CallbackExecutor Executor executor,
102             @NonNull ListCallback callback) {
103         registerListCallback(new ListCallback() {
104             public void onItemChanged(@NonNull ProgramSelector.Identifier id) {
105                 executor.execute(() -> callback.onItemChanged(id));
106             }
107 
108             public void onItemRemoved(@NonNull ProgramSelector.Identifier id) {
109                 executor.execute(() -> callback.onItemRemoved(id));
110             }
111         });
112     }
113 
114     /**
115      * Registers list change callback.
116      */
registerListCallback(@onNull ListCallback callback)117     public void registerListCallback(@NonNull ListCallback callback) {
118         synchronized (mLock) {
119             if (mIsClosed) return;
120             mListCallbacks.add(Objects.requireNonNull(callback));
121         }
122     }
123 
124     /**
125      * Unregisters list change callback.
126      */
unregisterListCallback(@onNull ListCallback callback)127     public void unregisterListCallback(@NonNull ListCallback callback) {
128         synchronized (mLock) {
129             if (mIsClosed) return;
130             mListCallbacks.remove(Objects.requireNonNull(callback));
131         }
132     }
133 
134     /**
135      * Adds list complete event listener with executor.
136      */
addOnCompleteListener(@onNull @allbackExecutor Executor executor, @NonNull OnCompleteListener listener)137     public void addOnCompleteListener(@NonNull @CallbackExecutor Executor executor,
138             @NonNull OnCompleteListener listener) {
139         addOnCompleteListener(() -> executor.execute(listener::onComplete));
140     }
141 
142     /**
143      * Adds list complete event listener.
144      */
addOnCompleteListener(@onNull OnCompleteListener listener)145     public void addOnCompleteListener(@NonNull OnCompleteListener listener) {
146         synchronized (mLock) {
147             if (mIsClosed) return;
148             mOnCompleteListeners.add(Objects.requireNonNull(listener));
149             if (mIsComplete) listener.onComplete();
150         }
151     }
152 
153     /**
154      * Removes list complete event listener.
155      */
removeOnCompleteListener(@onNull OnCompleteListener listener)156     public void removeOnCompleteListener(@NonNull OnCompleteListener listener) {
157         synchronized (mLock) {
158             if (mIsClosed) return;
159             mOnCompleteListeners.remove(Objects.requireNonNull(listener));
160         }
161     }
162 
setOnCloseListener(@ullable OnCloseListener listener)163     void setOnCloseListener(@Nullable OnCloseListener listener) {
164         synchronized (mLock) {
165             if (mOnCloseListener != null) {
166                 throw new IllegalStateException("Close callback is already set");
167             }
168             mOnCloseListener = listener;
169         }
170     }
171 
172     /**
173      * Disables list updates and releases all resources.
174      */
close()175     public void close() {
176         OnCloseListener onCompleteListenersCopied = null;
177         synchronized (mLock) {
178             if (mIsClosed) return;
179             mIsClosed = true;
180             mPrograms.clear();
181             mListCallbacks.clear();
182             mOnCompleteListeners.clear();
183             if (mOnCloseListener != null) {
184                 onCompleteListenersCopied = mOnCloseListener;
185                 mOnCloseListener = null;
186             }
187         }
188 
189         if (onCompleteListenersCopied != null) {
190             onCompleteListenersCopied.onClose();
191         }
192     }
193 
apply(Chunk chunk)194     void apply(Chunk chunk) {
195         List<ProgramSelector.Identifier> removedList = new ArrayList<>();
196         List<ProgramSelector.Identifier> changedList = new ArrayList<>();
197         List<ProgramList.ListCallback> listCallbacksCopied;
198         List<OnCompleteListener> onCompleteListenersCopied = new ArrayList<>();
199         synchronized (mLock) {
200             if (mIsClosed) return;
201 
202             mIsComplete = false;
203             listCallbacksCopied = new ArrayList<>(mListCallbacks);
204 
205             if (chunk.isPurge()) {
206                 Iterator<Map.Entry<ProgramSelector.Identifier, RadioManager.ProgramInfo>>
207                         programsIterator = mPrograms.entrySet().iterator();
208                 while (programsIterator.hasNext()) {
209                     RadioManager.ProgramInfo removed = programsIterator.next().getValue();
210                     if (removed != null) {
211                         removedList.add(removed.getSelector().getPrimaryId());
212                     }
213                     programsIterator.remove();
214                 }
215             }
216 
217             chunk.getRemoved().stream().forEach(id -> removeLocked(id, removedList));
218             chunk.getModified().stream().forEach(info -> putLocked(info, changedList));
219 
220             if (chunk.isComplete()) {
221                 mIsComplete = true;
222                 onCompleteListenersCopied = new ArrayList<>(mOnCompleteListeners);
223             }
224         }
225 
226         for (int i = 0; i < removedList.size(); i++) {
227             for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) {
228                 listCallbacksCopied.get(cbIndex).onItemRemoved(removedList.get(i));
229             }
230         }
231         for (int i = 0; i < changedList.size(); i++) {
232             for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) {
233                 listCallbacksCopied.get(cbIndex).onItemChanged(changedList.get(i));
234             }
235         }
236         if (chunk.isComplete()) {
237             for (int cbIndex = 0; cbIndex < onCompleteListenersCopied.size(); cbIndex++) {
238                 onCompleteListenersCopied.get(cbIndex).onComplete();
239             }
240         }
241     }
242 
243     @GuardedBy("mLock")
putLocked(RadioManager.ProgramInfo value, List<ProgramSelector.Identifier> changedIdentifierList)244     private void putLocked(RadioManager.ProgramInfo value,
245             List<ProgramSelector.Identifier> changedIdentifierList) {
246         ProgramSelector.Identifier key = value.getSelector().getPrimaryId();
247         mPrograms.put(Objects.requireNonNull(key), value);
248         ProgramSelector.Identifier sel = value.getSelector().getPrimaryId();
249         changedIdentifierList.add(sel);
250     }
251 
252     @GuardedBy("mLock")
removeLocked(ProgramSelector.Identifier key, List<ProgramSelector.Identifier> removedIdentifierList)253     private void removeLocked(ProgramSelector.Identifier key,
254             List<ProgramSelector.Identifier> removedIdentifierList) {
255         RadioManager.ProgramInfo removed = mPrograms.remove(Objects.requireNonNull(key));
256         if (removed == null) return;
257         ProgramSelector.Identifier sel = removed.getSelector().getPrimaryId();
258         removedIdentifierList.add(sel);
259     }
260 
261     /**
262      * Converts the program list in its current shape to the static List<>.
263      *
264      * @return the new List<> object; it won't receive any further updates
265      */
toList()266     public @NonNull List<RadioManager.ProgramInfo> toList() {
267         synchronized (mLock) {
268             return mPrograms.values().stream().collect(Collectors.toList());
269         }
270     }
271 
272     /**
273      * Returns the program with a specified primary identifier.
274      *
275      * @param id primary identifier of a program to fetch
276      * @return the program info, or null if there is no such program on the list
277      */
get(@onNull ProgramSelector.Identifier id)278     public @Nullable RadioManager.ProgramInfo get(@NonNull ProgramSelector.Identifier id) {
279         synchronized (mLock) {
280             return mPrograms.get(Objects.requireNonNull(id));
281         }
282     }
283 
284     /**
285      * Filter for the program list.
286      */
287     public static final class Filter implements Parcelable {
288         private final @NonNull Set<Integer> mIdentifierTypes;
289         private final @NonNull Set<ProgramSelector.Identifier> mIdentifiers;
290         private final boolean mIncludeCategories;
291         private final boolean mExcludeModifications;
292         private final @Nullable Map<String, String> mVendorFilter;
293 
294         /**
295          * Constructor of program list filter.
296          *
297          * Arrays passed to this constructor become owned by this object, do not modify them later.
298          *
299          * @param identifierTypes see getIdentifierTypes()
300          * @param identifiers see getIdentifiers()
301          * @param includeCategories see areCategoriesIncluded()
302          * @param excludeModifications see areModificationsExcluded()
303          */
Filter(@onNull Set<Integer> identifierTypes, @NonNull Set<ProgramSelector.Identifier> identifiers, boolean includeCategories, boolean excludeModifications)304         public Filter(@NonNull Set<Integer> identifierTypes,
305                 @NonNull Set<ProgramSelector.Identifier> identifiers,
306                 boolean includeCategories, boolean excludeModifications) {
307             mIdentifierTypes = Objects.requireNonNull(identifierTypes);
308             mIdentifiers = Objects.requireNonNull(identifiers);
309             mIncludeCategories = includeCategories;
310             mExcludeModifications = excludeModifications;
311             mVendorFilter = null;
312         }
313 
314         /**
315          * @hide for framework use only
316          */
Filter()317         public Filter() {
318             mIdentifierTypes = Collections.emptySet();
319             mIdentifiers = Collections.emptySet();
320             mIncludeCategories = false;
321             mExcludeModifications = false;
322             mVendorFilter = null;
323         }
324 
325         /**
326          * @hide for framework use only
327          */
Filter(@ullable Map<String, String> vendorFilter)328         public Filter(@Nullable Map<String, String> vendorFilter) {
329             mIdentifierTypes = Collections.emptySet();
330             mIdentifiers = Collections.emptySet();
331             mIncludeCategories = false;
332             mExcludeModifications = false;
333             mVendorFilter = vendorFilter;
334         }
335 
Filter(@onNull Parcel in)336         private Filter(@NonNull Parcel in) {
337             mIdentifierTypes = Utils.createIntSet(in);
338             mIdentifiers = Utils.createSet(in, ProgramSelector.Identifier.CREATOR);
339             mIncludeCategories = in.readByte() != 0;
340             mExcludeModifications = in.readByte() != 0;
341             mVendorFilter = Utils.readStringMap(in);
342         }
343 
344         @Override
writeToParcel(Parcel dest, int flags)345         public void writeToParcel(Parcel dest, int flags) {
346             Utils.writeIntSet(dest, mIdentifierTypes);
347             Utils.writeSet(dest, mIdentifiers);
348             dest.writeByte((byte) (mIncludeCategories ? 1 : 0));
349             dest.writeByte((byte) (mExcludeModifications ? 1 : 0));
350             Utils.writeStringMap(dest, mVendorFilter);
351         }
352 
353         @Override
describeContents()354         public int describeContents() {
355             return 0;
356         }
357 
358         public static final @android.annotation.NonNull Parcelable.Creator<Filter> CREATOR = new Parcelable.Creator<Filter>() {
359             public Filter createFromParcel(Parcel in) {
360                 return new Filter(in);
361             }
362 
363             public Filter[] newArray(int size) {
364                 return new Filter[size];
365             }
366         };
367 
368         /**
369          * @hide for framework use only
370          */
getVendorFilter()371         public Map<String, String> getVendorFilter() {
372             return mVendorFilter;
373         }
374 
375         /**
376          * Returns the list of identifier types that satisfy the filter.
377          *
378          * If the program list entry contains at least one identifier of the type
379          * listed, it satisfies this condition.
380          *
381          * Empty list means no filtering on identifier type.
382          *
383          * @return the list of accepted identifier types, must not be modified
384          */
getIdentifierTypes()385         public @NonNull Set<Integer> getIdentifierTypes() {
386             return mIdentifierTypes;
387         }
388 
389         /**
390          * Returns the list of identifiers that satisfy the filter.
391          *
392          * If the program list entry contains at least one listed identifier,
393          * it satisfies this condition.
394          *
395          * Empty list means no filtering on identifier.
396          *
397          * @return the list of accepted identifiers, must not be modified
398          */
getIdentifiers()399         public @NonNull Set<ProgramSelector.Identifier> getIdentifiers() {
400             return mIdentifiers;
401         }
402 
403         /**
404          * Checks, if non-tunable entries that define tree structure on the
405          * program list (i.e. DAB ensembles) should be included.
406          *
407          * @see {@link ProgramSelector.Identifier#isCategory()}
408          */
areCategoriesIncluded()409         public boolean areCategoriesIncluded() {
410             return mIncludeCategories;
411         }
412 
413         /**
414          * Checks, if updates on entry modifications should be disabled.
415          *
416          * If true, 'modified' vector of ProgramListChunk must contain list
417          * additions only. Once the program is added to the list, it's not
418          * updated anymore.
419          */
areModificationsExcluded()420         public boolean areModificationsExcluded() {
421             return mExcludeModifications;
422         }
423 
424         @Override
hashCode()425         public int hashCode() {
426             return Objects.hash(mIdentifierTypes, mIdentifiers, mIncludeCategories,
427                     mExcludeModifications);
428         }
429 
430         @Override
equals(@ullable Object obj)431         public boolean equals(@Nullable Object obj) {
432             if (this == obj) return true;
433             if (!(obj instanceof Filter)) return false;
434             Filter other = (Filter) obj;
435 
436             if (mIncludeCategories != other.mIncludeCategories) return false;
437             if (mExcludeModifications != other.mExcludeModifications) return false;
438             if (!Objects.equals(mIdentifierTypes, other.mIdentifierTypes)) return false;
439             if (!Objects.equals(mIdentifiers, other.mIdentifiers)) return false;
440             return true;
441         }
442 
443         @NonNull
444         @Override
toString()445         public String toString() {
446             return "Filter [mIdentifierTypes=" + mIdentifierTypes
447                     + ", mIdentifiers=" + mIdentifiers
448                     + ", mIncludeCategories=" + mIncludeCategories
449                     + ", mExcludeModifications=" + mExcludeModifications + "]";
450         }
451     }
452 
453     /**
454      * @hide This is a transport class used for internal communication between
455      *       Broadcast Radio Service and RadioManager.
456      *       Do not use it directly.
457      */
458     public static final class Chunk implements Parcelable {
459         private final boolean mPurge;
460         private final boolean mComplete;
461         private final @NonNull Set<RadioManager.ProgramInfo> mModified;
462         private final @NonNull Set<ProgramSelector.Identifier> mRemoved;
463 
Chunk(boolean purge, boolean complete, @Nullable Set<RadioManager.ProgramInfo> modified, @Nullable Set<ProgramSelector.Identifier> removed)464         public Chunk(boolean purge, boolean complete,
465                 @Nullable Set<RadioManager.ProgramInfo> modified,
466                 @Nullable Set<ProgramSelector.Identifier> removed) {
467             mPurge = purge;
468             mComplete = complete;
469             mModified = (modified != null) ? modified : Collections.emptySet();
470             mRemoved = (removed != null) ? removed : Collections.emptySet();
471         }
472 
Chunk(@onNull Parcel in)473         private Chunk(@NonNull Parcel in) {
474             mPurge = in.readByte() != 0;
475             mComplete = in.readByte() != 0;
476             mModified = Utils.createSet(in, RadioManager.ProgramInfo.CREATOR);
477             mRemoved = Utils.createSet(in, ProgramSelector.Identifier.CREATOR);
478         }
479 
480         @Override
writeToParcel(Parcel dest, int flags)481         public void writeToParcel(Parcel dest, int flags) {
482             dest.writeByte((byte) (mPurge ? 1 : 0));
483             dest.writeByte((byte) (mComplete ? 1 : 0));
484             Utils.writeSet(dest, mModified);
485             Utils.writeSet(dest, mRemoved);
486         }
487 
488         @Override
describeContents()489         public int describeContents() {
490             return 0;
491         }
492 
493         public static final @android.annotation.NonNull Parcelable.Creator<Chunk> CREATOR = new Parcelable.Creator<Chunk>() {
494             public Chunk createFromParcel(Parcel in) {
495                 return new Chunk(in);
496             }
497 
498             public Chunk[] newArray(int size) {
499                 return new Chunk[size];
500             }
501         };
502 
isPurge()503         public boolean isPurge() {
504             return mPurge;
505         }
506 
isComplete()507         public boolean isComplete() {
508             return mComplete;
509         }
510 
getModified()511         public @NonNull Set<RadioManager.ProgramInfo> getModified() {
512             return mModified;
513         }
514 
getRemoved()515         public @NonNull Set<ProgramSelector.Identifier> getRemoved() {
516             return mRemoved;
517         }
518 
519         @Override
equals(@ullable Object obj)520         public boolean equals(@Nullable Object obj) {
521             if (this == obj) return true;
522             if (!(obj instanceof Chunk)) return false;
523             Chunk other = (Chunk) obj;
524 
525             if (mPurge != other.mPurge) return false;
526             if (mComplete != other.mComplete) return false;
527             if (!Objects.equals(mModified, other.mModified)) return false;
528             if (!Objects.equals(mRemoved, other.mRemoved)) return false;
529             return true;
530         }
531 
532         @Override
toString()533         public String toString() {
534             return "Chunk [mPurge=" + mPurge + ", mComplete=" + mComplete
535                     + ", mModified=" + mModified + ", mRemoved=" + mRemoved + "]";
536         }
537     }
538 }
539