• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (C) 2019 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 com.android.server.broadcastradio.hal2;
18 
19 import android.annotation.Nullable;
20 import android.hardware.broadcastradio.V2_0.ProgramListChunk;
21 import android.hardware.radio.ProgramList;
22 import android.hardware.radio.ProgramSelector.Identifier;
23 import android.hardware.radio.RadioManager;
24 import android.hardware.radio.UniqueProgramIdentifier;
25 import android.util.ArrayMap;
26 import android.util.ArraySet;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 
30 import java.util.ArrayList;
31 import java.util.Collection;
32 import java.util.Iterator;
33 import java.util.List;
34 import java.util.Set;
35 
36 final class ProgramInfoCache {
37     // Maximum number of RadioManager.ProgramInfo elements that will be put into a
38     // ProgramList.Chunk.mModified array. Used to try to ensure a single ProgramList.Chunk stays
39     // within the AIDL data size limit.
40     private static final int MAX_NUM_MODIFIED_PER_CHUNK = 100;
41 
42     // Maximum number of ProgramSelector.Identifier elements that will be put into a
43     // ProgramList.Chunk.mRemoved array. Use to attempt and keep the single ProgramList.Chunk
44     // within the AIDL data size limit.
45     private static final int MAX_NUM_REMOVED_PER_CHUNK = 500;
46 
47     // Map from primary identifier to a map of unique identifiers and program info, where the
48     // containing map has unique identifiers to program info.
49     private final ArrayMap<Identifier, ArrayMap<UniqueProgramIdentifier, RadioManager.ProgramInfo>>
50             mProgramInfoMap = new ArrayMap<>();
51 
52     // Flag indicating whether mProgramInfoMap is considered complete based upon the received
53     // updates.
54     private boolean mComplete = true;
55 
56     // Optional filter used in filterAndUpdateFrom(). Usually this field is null for a HAL-side
57     // cache and non-null for an AIDL-side cache.
58     private final ProgramList.Filter mFilter;
59 
ProgramInfoCache(@ullable ProgramList.Filter filter)60     ProgramInfoCache(@Nullable ProgramList.Filter filter) {
61         mFilter = filter;
62     }
63 
64     // Constructor for testing.
65     @VisibleForTesting
ProgramInfoCache(@ullable ProgramList.Filter filter, boolean complete, RadioManager.ProgramInfo... programInfos)66     ProgramInfoCache(@Nullable ProgramList.Filter filter, boolean complete,
67             RadioManager.ProgramInfo... programInfos) {
68         mFilter = filter;
69         mComplete = complete;
70         for (int i = 0; i < programInfos.length; i++) {
71             putInfo(programInfos[i]);
72         }
73     }
74 
75     @VisibleForTesting
toProgramInfoList()76     List<RadioManager.ProgramInfo> toProgramInfoList() {
77         List<RadioManager.ProgramInfo> programInfoList = new ArrayList<>();
78         for (int index = 0; index < mProgramInfoMap.size(); index++) {
79             programInfoList.addAll(mProgramInfoMap.valueAt(index).values());
80         }
81         return programInfoList;
82     }
83 
84     @Override
toString()85     public String toString() {
86         StringBuilder sb = new StringBuilder("ProgramInfoCache(mComplete = ");
87         sb.append(mComplete);
88         sb.append(", mFilter = ");
89         sb.append(mFilter);
90         sb.append(", mProgramInfoMap = [");
91         for (int index = 0; index < mProgramInfoMap.size(); index++) {
92             ArrayMap<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries =
93                     mProgramInfoMap.valueAt(index);
94             for (int entryIndex = 0; entryIndex < entries.size(); entryIndex++) {
95                 sb.append(", ");
96                 sb.append(entries.valueAt(entryIndex));
97             }
98         }
99         sb.append("]");
100         return sb.toString();
101     }
102 
isComplete()103     public boolean isComplete() {
104         return mComplete;
105     }
106 
getFilter()107     public @Nullable ProgramList.Filter getFilter() {
108         return mFilter;
109     }
110 
updateFromHalProgramListChunk(ProgramListChunk chunk)111     void updateFromHalProgramListChunk(ProgramListChunk chunk) {
112         if (chunk.purge) {
113             mProgramInfoMap.clear();
114         }
115         for (android.hardware.broadcastradio.V2_0.ProgramInfo halProgramInfo : chunk.modified) {
116             RadioManager.ProgramInfo programInfo = Convert.programInfoFromHal(halProgramInfo);
117             putInfo(programInfo);
118         }
119         for (android.hardware.broadcastradio.V2_0.ProgramIdentifier halProgramId : chunk.removed) {
120             mProgramInfoMap.remove(Convert.programIdentifierFromHal(halProgramId));
121         }
122         mComplete = chunk.complete;
123     }
124 
filterAndUpdateFrom(ProgramInfoCache other, boolean purge)125     List<ProgramList.Chunk> filterAndUpdateFrom(ProgramInfoCache other, boolean purge) {
126         return filterAndUpdateFromInternal(other, purge, MAX_NUM_MODIFIED_PER_CHUNK,
127                 MAX_NUM_REMOVED_PER_CHUNK);
128     }
129 
130     @VisibleForTesting
filterAndUpdateFromInternal(ProgramInfoCache other, boolean purge, int maxNumModifiedPerChunk, int maxNumRemovedPerChunk)131     List<ProgramList.Chunk> filterAndUpdateFromInternal(ProgramInfoCache other,
132             boolean purge, int maxNumModifiedPerChunk, int maxNumRemovedPerChunk) {
133         if (purge) {
134             mProgramInfoMap.clear();
135         }
136         // If mProgramInfoMap is empty, we treat this update as a purge because this might be the
137         // first update to an AIDL client that changed its filter.
138         if (mProgramInfoMap.isEmpty()) {
139             purge = true;
140         }
141 
142         ArraySet<RadioManager.ProgramInfo> modified = new ArraySet<>();
143         ArraySet<UniqueProgramIdentifier> removed = new ArraySet<>();
144         for (int index = 0; index < mProgramInfoMap.size(); index++) {
145             removed.addAll(mProgramInfoMap.valueAt(index).keySet());
146         }
147         for (int index = 0; index < other.mProgramInfoMap.size(); index++) {
148             Identifier id = other.mProgramInfoMap.keyAt(index);
149             if (!passesFilter(id)) {
150                 continue;
151             }
152             ArrayMap<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries =
153                     other.mProgramInfoMap.valueAt(index);
154             for (int entryIndex = 0; entryIndex < entries.size(); entryIndex++) {
155                 removed.remove(entries.keyAt(entryIndex));
156 
157                 RadioManager.ProgramInfo newInfo = entries.valueAt(entryIndex);
158                 if (!shouldIncludeInModified(newInfo)) {
159                     continue;
160                 }
161                 putInfo(newInfo);
162                 modified.add(newInfo);
163             }
164         }
165         for (int removedIndex = 0; removedIndex < removed.size(); removedIndex++) {
166             removeUniqueId(removed.valueAt(removedIndex));
167         }
168         mComplete = other.mComplete;
169         return buildChunks(purge, mComplete, modified, maxNumModifiedPerChunk, removed,
170                 maxNumRemovedPerChunk);
171     }
172 
173     @Nullable
filterAndApplyChunk(ProgramListChunk chunk)174     List<ProgramList.Chunk> filterAndApplyChunk(ProgramListChunk chunk) {
175         return filterAndApplyChunkInternal(chunk, MAX_NUM_MODIFIED_PER_CHUNK,
176                 MAX_NUM_REMOVED_PER_CHUNK);
177     }
178 
179     @VisibleForTesting
180     @Nullable
filterAndApplyChunkInternal(ProgramListChunk chunk, int maxNumModifiedPerChunk, int maxNumRemovedPerChunk)181     List<ProgramList.Chunk> filterAndApplyChunkInternal(ProgramListChunk chunk,
182             int maxNumModifiedPerChunk, int maxNumRemovedPerChunk) {
183         if (chunk.purge) {
184             mProgramInfoMap.clear();
185         }
186 
187         Set<RadioManager.ProgramInfo> modified = new ArraySet<>();
188         for (android.hardware.broadcastradio.V2_0.ProgramInfo halProgramInfo : chunk.modified) {
189             RadioManager.ProgramInfo info = Convert.programInfoFromHal(halProgramInfo);
190             Identifier primaryId = info.getSelector().getPrimaryId();
191             if (!passesFilter(primaryId) || !shouldIncludeInModified(info)) {
192                 continue;
193             }
194             putInfo(info);
195             modified.add(info);
196         }
197         Set<UniqueProgramIdentifier> removed = new ArraySet<>();
198         for (android.hardware.broadcastradio.V2_0.ProgramIdentifier halProgramId : chunk.removed) {
199             Identifier removedId = Convert.programIdentifierFromHal(halProgramId);
200             if (removedId == null) {
201                 continue;
202             }
203             if (mProgramInfoMap.containsKey(removedId)) {
204                 removed.addAll(mProgramInfoMap.get(removedId).keySet());
205                 mProgramInfoMap.remove(removedId);
206             }
207         }
208         if (modified.isEmpty() && removed.isEmpty() && mComplete == chunk.complete
209                 && !chunk.purge) {
210             return null;
211         }
212         mComplete = chunk.complete;
213         return buildChunks(chunk.purge, mComplete, modified, maxNumModifiedPerChunk, removed,
214                 maxNumRemovedPerChunk);
215     }
216 
passesFilter(Identifier id)217     private boolean passesFilter(Identifier id) {
218         if (mFilter == null) {
219             return true;
220         }
221         if (!mFilter.getIdentifierTypes().isEmpty()
222                 && !mFilter.getIdentifierTypes().contains(id.getType())) {
223             return false;
224         }
225         if (!mFilter.getIdentifiers().isEmpty() && !mFilter.getIdentifiers().contains(id)) {
226             return false;
227         }
228         if (!mFilter.areCategoriesIncluded() && id.isCategoryType()) {
229             return false;
230         }
231         return true;
232     }
233 
putInfo(RadioManager.ProgramInfo info)234     private void putInfo(RadioManager.ProgramInfo info) {
235         Identifier primaryId = info.getSelector().getPrimaryId();
236         if (!mProgramInfoMap.containsKey(primaryId)) {
237             mProgramInfoMap.put(primaryId, new ArrayMap<>());
238         }
239         mProgramInfoMap.get(primaryId).put(new UniqueProgramIdentifier(
240                 info.getSelector()), info);
241     }
242 
removeUniqueId(UniqueProgramIdentifier uniqueId)243     private void removeUniqueId(UniqueProgramIdentifier uniqueId) {
244         Identifier primaryId =  uniqueId.getPrimaryId();
245         if (!mProgramInfoMap.containsKey(primaryId)) {
246             return;
247         }
248         mProgramInfoMap.get(primaryId).remove(uniqueId);
249         if (mProgramInfoMap.get(primaryId).isEmpty()) {
250             mProgramInfoMap.remove(primaryId);
251         }
252     }
253 
shouldIncludeInModified(RadioManager.ProgramInfo newInfo)254     private boolean shouldIncludeInModified(RadioManager.ProgramInfo newInfo) {
255         Identifier primaryId = newInfo.getSelector().getPrimaryId();
256         RadioManager.ProgramInfo oldInfo = null;
257         if (mProgramInfoMap.containsKey(primaryId)) {
258             UniqueProgramIdentifier uniqueId = new UniqueProgramIdentifier(newInfo.getSelector());
259             oldInfo = mProgramInfoMap.get(primaryId).get(uniqueId);
260         }
261         if (oldInfo == null) {
262             return true;
263         }
264         if (mFilter != null && mFilter.areModificationsExcluded()) {
265             return false;
266         }
267         return !oldInfo.equals(newInfo);
268     }
269 
roundUpFraction(int numerator, int denominator)270     private static int roundUpFraction(int numerator, int denominator) {
271         return (numerator / denominator) + (numerator % denominator > 0 ? 1 : 0);
272     }
273 
buildChunks(boolean purge, boolean complete, @Nullable Collection<RadioManager.ProgramInfo> modified, int maxNumModifiedPerChunk, @Nullable Collection<UniqueProgramIdentifier> removed, int maxNumRemovedPerChunk)274     private static List<ProgramList.Chunk> buildChunks(boolean purge, boolean complete,
275             @Nullable Collection<RadioManager.ProgramInfo> modified, int maxNumModifiedPerChunk,
276             @Nullable Collection<UniqueProgramIdentifier> removed, int maxNumRemovedPerChunk) {
277         // Communication protocol requires that if purge is set, removed is empty.
278         if (purge) {
279             removed = null;
280         }
281 
282         // Determine number of chunks we need to send.
283         int numChunks = purge ? 1 : 0;
284         if (modified != null) {
285             numChunks = Math.max(numChunks,
286                     roundUpFraction(modified.size(), maxNumModifiedPerChunk));
287         }
288         if (removed != null) {
289             numChunks = Math.max(numChunks, roundUpFraction(removed.size(), maxNumRemovedPerChunk));
290         }
291         if (numChunks == 0) {
292             return new ArrayList<ProgramList.Chunk>();
293         }
294 
295         // Try to make similarly-sized chunks by evenly distributing elements from modified and
296         // removed among them.
297         int modifiedPerChunk = 0;
298         int removedPerChunk = 0;
299         Iterator<RadioManager.ProgramInfo> modifiedIter = null;
300         Iterator<UniqueProgramIdentifier> removedIter = null;
301         if (modified != null) {
302             modifiedPerChunk = roundUpFraction(modified.size(), numChunks);
303             modifiedIter = modified.iterator();
304         }
305         if (removed != null) {
306             removedPerChunk = roundUpFraction(removed.size(), numChunks);
307             removedIter = removed.iterator();
308         }
309         List<ProgramList.Chunk> chunks = new ArrayList<ProgramList.Chunk>(numChunks);
310         for (int i = 0; i < numChunks; i++) {
311             ArraySet<RadioManager.ProgramInfo> modifiedChunk = new ArraySet<>();
312             ArraySet<UniqueProgramIdentifier> removedChunk = new ArraySet<>();
313             if (modifiedIter != null) {
314                 for (int j = 0; j < modifiedPerChunk && modifiedIter.hasNext(); j++) {
315                     modifiedChunk.add(modifiedIter.next());
316                 }
317             }
318             if (removedIter != null) {
319                 for (int j = 0; j < removedPerChunk && removedIter.hasNext(); j++) {
320                     removedChunk.add(removedIter.next());
321                 }
322             }
323             chunks.add(new ProgramList.Chunk(purge && i == 0, complete && (i == numChunks - 1),
324                       modifiedChunk, removedChunk));
325         }
326         return chunks;
327     }
328 }
329