ProgramList.java revision d0c78f9f28599ac97dbca06c1634238f9b25be40
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
17package android.hardware.radio;
18
19import android.annotation.CallbackExecutor;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.annotation.SystemApi;
23import android.os.Parcel;
24import android.os.Parcelable;
25
26import java.util.ArrayList;
27import java.util.Collections;
28import java.util.HashMap;
29import java.util.HashSet;
30import java.util.List;
31import java.util.Map;
32import java.util.Objects;
33import java.util.Set;
34import java.util.concurrent.Executor;
35import java.util.stream.Collectors;
36
37/**
38 * @hide
39 */
40@SystemApi
41public 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
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         */
62        public void onItemChanged(@NonNull ProgramSelector.Identifier id) { }
63
64        /**
65         * Called when item was removed from the list.
66         */
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         */
78        void onComplete();
79    }
80
81    interface OnCloseListener {
82        void onClose();
83    }
84
85    /**
86     * Registers list change callback with executor.
87     */
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     */
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     */
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     */
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     */
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     */
143    public void removeOnCompleteListener(@NonNull OnCompleteListener listener) {
144        synchronized (mLock) {
145            if (mIsClosed) return;
146            mOnCompleteListeners.remove(Objects.requireNonNull(listener));
147        }
148    }
149
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     */
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
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
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
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     */
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     */
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         */
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         */
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         */
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
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
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
303        public int describeContents() {
304            return 0;
305        }
306
307        public static final 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         */
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         */
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         */
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        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         */
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
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
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
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
408        public int describeContents() {
409            return 0;
410        }
411
412        public static final 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
422        public boolean isPurge() {
423            return mPurge;
424        }
425
426        public boolean isComplete() {
427            return mComplete;
428        }
429
430        public @NonNull Set<RadioManager.ProgramInfo> getModified() {
431            return mModified;
432        }
433
434        public @NonNull Set<ProgramSelector.Identifier> getRemoved() {
435            return mRemoved;
436        }
437    }
438}
439