• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 java.util.ArrayList;
28 import java.util.Collections;
29 import java.util.Iterator;
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 ArrayMap<>();
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         OnCloseListener onCompleteListenersCopied = null;
164         synchronized (mLock) {
165             if (mIsClosed) return;
166             mIsClosed = true;
167             mPrograms.clear();
168             mListCallbacks.clear();
169             mOnCompleteListeners.clear();
170             if (mOnCloseListener != null) {
171                 onCompleteListenersCopied = mOnCloseListener;
172                 mOnCloseListener = null;
173             }
174         }
175 
176         if (onCompleteListenersCopied != null) {
177             onCompleteListenersCopied.onClose();
178         }
179     }
180 
apply(Chunk chunk)181     void apply(Chunk chunk) {
182         List<ProgramSelector.Identifier> removedList = new ArrayList<>();
183         List<ProgramSelector.Identifier> changedList = new ArrayList<>();
184         List<ProgramList.ListCallback> listCallbacksCopied;
185         List<OnCompleteListener> onCompleteListenersCopied = new ArrayList<>();
186         synchronized (mLock) {
187             if (mIsClosed) return;
188 
189             mIsComplete = false;
190             listCallbacksCopied = new ArrayList<>(mListCallbacks);
191 
192             if (chunk.isPurge()) {
193                 Iterator<Map.Entry<ProgramSelector.Identifier, RadioManager.ProgramInfo>>
194                         programsIterator = mPrograms.entrySet().iterator();
195                 while (programsIterator.hasNext()) {
196                     RadioManager.ProgramInfo removed = programsIterator.next().getValue();
197                     if (removed != null) {
198                         removedList.add(removed.getSelector().getPrimaryId());
199                     }
200                     programsIterator.remove();
201                 }
202             }
203 
204             chunk.getRemoved().stream().forEach(id -> removeLocked(id, removedList));
205             chunk.getModified().stream().forEach(info -> putLocked(info, changedList));
206 
207             if (chunk.isComplete()) {
208                 mIsComplete = true;
209                 onCompleteListenersCopied = new ArrayList<>(mOnCompleteListeners);
210             }
211         }
212 
213         for (int i = 0; i < removedList.size(); i++) {
214             for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) {
215                 listCallbacksCopied.get(cbIndex).onItemRemoved(removedList.get(i));
216             }
217         }
218         for (int i = 0; i < changedList.size(); i++) {
219             for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) {
220                 listCallbacksCopied.get(cbIndex).onItemChanged(changedList.get(i));
221             }
222         }
223         if (chunk.isComplete()) {
224             for (int cbIndex = 0; cbIndex < onCompleteListenersCopied.size(); cbIndex++) {
225                 onCompleteListenersCopied.get(cbIndex).onComplete();
226             }
227         }
228     }
229 
putLocked(RadioManager.ProgramInfo value, List<ProgramSelector.Identifier> changedIdentifierList)230     private void putLocked(RadioManager.ProgramInfo value,
231             List<ProgramSelector.Identifier> changedIdentifierList) {
232         ProgramSelector.Identifier key = value.getSelector().getPrimaryId();
233         mPrograms.put(Objects.requireNonNull(key), value);
234         ProgramSelector.Identifier sel = value.getSelector().getPrimaryId();
235         changedIdentifierList.add(sel);
236     }
237 
removeLocked(ProgramSelector.Identifier key, List<ProgramSelector.Identifier> removedIdentifierList)238     private void removeLocked(ProgramSelector.Identifier key,
239             List<ProgramSelector.Identifier> removedIdentifierList) {
240         RadioManager.ProgramInfo removed = mPrograms.remove(Objects.requireNonNull(key));
241         if (removed == null) return;
242         ProgramSelector.Identifier sel = removed.getSelector().getPrimaryId();
243         removedIdentifierList.add(sel);
244     }
245 
246     /**
247      * Converts the program list in its current shape to the static List<>.
248      *
249      * @return the new List<> object; it won't receive any further updates
250      */
toList()251     public @NonNull List<RadioManager.ProgramInfo> toList() {
252         synchronized (mLock) {
253             return mPrograms.values().stream().collect(Collectors.toList());
254         }
255     }
256 
257     /**
258      * Returns the program with a specified primary identifier.
259      *
260      * @param id primary identifier of a program to fetch
261      * @return the program info, or null if there is no such program on the list
262      */
get(@onNull ProgramSelector.Identifier id)263     public @Nullable RadioManager.ProgramInfo get(@NonNull ProgramSelector.Identifier id) {
264         synchronized (mLock) {
265             return mPrograms.get(Objects.requireNonNull(id));
266         }
267     }
268 
269     /**
270      * Filter for the program list.
271      */
272     public static final class Filter implements Parcelable {
273         private final @NonNull Set<Integer> mIdentifierTypes;
274         private final @NonNull Set<ProgramSelector.Identifier> mIdentifiers;
275         private final boolean mIncludeCategories;
276         private final boolean mExcludeModifications;
277         private final @Nullable Map<String, String> mVendorFilter;
278 
279         /**
280          * Constructor of program list filter.
281          *
282          * Arrays passed to this constructor become owned by this object, do not modify them later.
283          *
284          * @param identifierTypes see getIdentifierTypes()
285          * @param identifiers see getIdentifiers()
286          * @param includeCategories see areCategoriesIncluded()
287          * @param excludeModifications see areModificationsExcluded()
288          */
Filter(@onNull Set<Integer> identifierTypes, @NonNull Set<ProgramSelector.Identifier> identifiers, boolean includeCategories, boolean excludeModifications)289         public Filter(@NonNull Set<Integer> identifierTypes,
290                 @NonNull Set<ProgramSelector.Identifier> identifiers,
291                 boolean includeCategories, boolean excludeModifications) {
292             mIdentifierTypes = Objects.requireNonNull(identifierTypes);
293             mIdentifiers = Objects.requireNonNull(identifiers);
294             mIncludeCategories = includeCategories;
295             mExcludeModifications = excludeModifications;
296             mVendorFilter = null;
297         }
298 
299         /**
300          * @hide for framework use only
301          */
Filter()302         public Filter() {
303             mIdentifierTypes = Collections.emptySet();
304             mIdentifiers = Collections.emptySet();
305             mIncludeCategories = false;
306             mExcludeModifications = false;
307             mVendorFilter = null;
308         }
309 
310         /**
311          * @hide for framework use only
312          */
Filter(@ullable Map<String, String> vendorFilter)313         public Filter(@Nullable Map<String, String> vendorFilter) {
314             mIdentifierTypes = Collections.emptySet();
315             mIdentifiers = Collections.emptySet();
316             mIncludeCategories = false;
317             mExcludeModifications = false;
318             mVendorFilter = vendorFilter;
319         }
320 
Filter(@onNull Parcel in)321         private Filter(@NonNull Parcel in) {
322             mIdentifierTypes = Utils.createIntSet(in);
323             mIdentifiers = Utils.createSet(in, ProgramSelector.Identifier.CREATOR);
324             mIncludeCategories = in.readByte() != 0;
325             mExcludeModifications = in.readByte() != 0;
326             mVendorFilter = Utils.readStringMap(in);
327         }
328 
329         @Override
writeToParcel(Parcel dest, int flags)330         public void writeToParcel(Parcel dest, int flags) {
331             Utils.writeIntSet(dest, mIdentifierTypes);
332             Utils.writeSet(dest, mIdentifiers);
333             dest.writeByte((byte) (mIncludeCategories ? 1 : 0));
334             dest.writeByte((byte) (mExcludeModifications ? 1 : 0));
335             Utils.writeStringMap(dest, mVendorFilter);
336         }
337 
338         @Override
describeContents()339         public int describeContents() {
340             return 0;
341         }
342 
343         public static final @android.annotation.NonNull Parcelable.Creator<Filter> CREATOR = new Parcelable.Creator<Filter>() {
344             public Filter createFromParcel(Parcel in) {
345                 return new Filter(in);
346             }
347 
348             public Filter[] newArray(int size) {
349                 return new Filter[size];
350             }
351         };
352 
353         /**
354          * @hide for framework use only
355          */
getVendorFilter()356         public Map<String, String> getVendorFilter() {
357             return mVendorFilter;
358         }
359 
360         /**
361          * Returns the list of identifier types that satisfy the filter.
362          *
363          * If the program list entry contains at least one identifier of the type
364          * listed, it satisfies this condition.
365          *
366          * Empty list means no filtering on identifier type.
367          *
368          * @return the list of accepted identifier types, must not be modified
369          */
getIdentifierTypes()370         public @NonNull Set<Integer> getIdentifierTypes() {
371             return mIdentifierTypes;
372         }
373 
374         /**
375          * Returns the list of identifiers that satisfy the filter.
376          *
377          * If the program list entry contains at least one listed identifier,
378          * it satisfies this condition.
379          *
380          * Empty list means no filtering on identifier.
381          *
382          * @return the list of accepted identifiers, must not be modified
383          */
getIdentifiers()384         public @NonNull Set<ProgramSelector.Identifier> getIdentifiers() {
385             return mIdentifiers;
386         }
387 
388         /**
389          * Checks, if non-tunable entries that define tree structure on the
390          * program list (i.e. DAB ensembles) should be included.
391          *
392          * @see {@link ProgramSelector.Identifier#isCategory()}
393          */
areCategoriesIncluded()394         public boolean areCategoriesIncluded() {
395             return mIncludeCategories;
396         }
397 
398         /**
399          * Checks, if updates on entry modifications should be disabled.
400          *
401          * If true, 'modified' vector of ProgramListChunk must contain list
402          * additions only. Once the program is added to the list, it's not
403          * updated anymore.
404          */
areModificationsExcluded()405         public boolean areModificationsExcluded() {
406             return mExcludeModifications;
407         }
408 
409         @Override
hashCode()410         public int hashCode() {
411             return Objects.hash(mIdentifierTypes, mIdentifiers, mIncludeCategories,
412                     mExcludeModifications);
413         }
414 
415         @Override
equals(@ullable Object obj)416         public boolean equals(@Nullable Object obj) {
417             if (this == obj) return true;
418             if (!(obj instanceof Filter)) return false;
419             Filter other = (Filter) obj;
420 
421             if (mIncludeCategories != other.mIncludeCategories) return false;
422             if (mExcludeModifications != other.mExcludeModifications) return false;
423             if (!Objects.equals(mIdentifierTypes, other.mIdentifierTypes)) return false;
424             if (!Objects.equals(mIdentifiers, other.mIdentifiers)) return false;
425             return true;
426         }
427 
428         @NonNull
429         @Override
toString()430         public String toString() {
431             return "Filter [mIdentifierTypes=" + mIdentifierTypes
432                     + ", mIdentifiers=" + mIdentifiers
433                     + ", mIncludeCategories=" + mIncludeCategories
434                     + ", mExcludeModifications=" + mExcludeModifications + "]";
435         }
436     }
437 
438     /**
439      * @hide This is a transport class used for internal communication between
440      *       Broadcast Radio Service and RadioManager.
441      *       Do not use it directly.
442      */
443     public static final class Chunk implements Parcelable {
444         private final boolean mPurge;
445         private final boolean mComplete;
446         private final @NonNull Set<RadioManager.ProgramInfo> mModified;
447         private final @NonNull Set<ProgramSelector.Identifier> mRemoved;
448 
Chunk(boolean purge, boolean complete, @Nullable Set<RadioManager.ProgramInfo> modified, @Nullable Set<ProgramSelector.Identifier> removed)449         public Chunk(boolean purge, boolean complete,
450                 @Nullable Set<RadioManager.ProgramInfo> modified,
451                 @Nullable Set<ProgramSelector.Identifier> removed) {
452             mPurge = purge;
453             mComplete = complete;
454             mModified = (modified != null) ? modified : Collections.emptySet();
455             mRemoved = (removed != null) ? removed : Collections.emptySet();
456         }
457 
Chunk(@onNull Parcel in)458         private Chunk(@NonNull Parcel in) {
459             mPurge = in.readByte() != 0;
460             mComplete = in.readByte() != 0;
461             mModified = Utils.createSet(in, RadioManager.ProgramInfo.CREATOR);
462             mRemoved = Utils.createSet(in, ProgramSelector.Identifier.CREATOR);
463         }
464 
465         @Override
writeToParcel(Parcel dest, int flags)466         public void writeToParcel(Parcel dest, int flags) {
467             dest.writeByte((byte) (mPurge ? 1 : 0));
468             dest.writeByte((byte) (mComplete ? 1 : 0));
469             Utils.writeSet(dest, mModified);
470             Utils.writeSet(dest, mRemoved);
471         }
472 
473         @Override
describeContents()474         public int describeContents() {
475             return 0;
476         }
477 
478         public static final @android.annotation.NonNull Parcelable.Creator<Chunk> CREATOR = new Parcelable.Creator<Chunk>() {
479             public Chunk createFromParcel(Parcel in) {
480                 return new Chunk(in);
481             }
482 
483             public Chunk[] newArray(int size) {
484                 return new Chunk[size];
485             }
486         };
487 
isPurge()488         public boolean isPurge() {
489             return mPurge;
490         }
491 
isComplete()492         public boolean isComplete() {
493             return mComplete;
494         }
495 
getModified()496         public @NonNull Set<RadioManager.ProgramInfo> getModified() {
497             return mModified;
498         }
499 
getRemoved()500         public @NonNull Set<ProgramSelector.Identifier> getRemoved() {
501             return mRemoved;
502         }
503 
504         @Override
equals(@ullable Object obj)505         public boolean equals(@Nullable Object obj) {
506             if (this == obj) return true;
507             if (!(obj instanceof Chunk)) return false;
508             Chunk other = (Chunk) obj;
509 
510             if (mPurge != other.mPurge) return false;
511             if (mComplete != other.mComplete) return false;
512             if (!Objects.equals(mModified, other.mModified)) return false;
513             if (!Objects.equals(mRemoved, other.mRemoved)) return false;
514             return true;
515         }
516 
517         @Override
toString()518         public String toString() {
519             return "Chunk [mPurge=" + mPurge + ", mComplete=" + mComplete
520                     + ", mModified=" + mModified + ", mRemoved=" + mRemoved + "]";
521         }
522     }
523 }
524