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