• 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.NonNull;
20 import android.annotation.Nullable;
21 import android.hardware.radio.ProgramList;
22 import android.hardware.radio.ProgramSelector;
23 import android.hardware.radio.RadioManager;
24 
25 import com.android.internal.annotations.VisibleForTesting;
26 
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.HashMap;
30 import java.util.HashSet;
31 import java.util.Iterator;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35 
36 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. Used to try to ensure a single ProgramList.Chunk stays
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 corresponding ProgramInfo.
48     private final Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> mProgramInfoMap =
49             new HashMap<>();
50 
51     // Flag indicating whether mProgramInfoMap is considered complete based upon the received
52     // updates.
53     private boolean mComplete = true;
54 
55     // Optional filter used in filterAndUpdateFrom(). Usually this field is null for a HAL-side
56     // cache and non-null for an AIDL-side cache.
57     private final ProgramList.Filter mFilter;
58 
ProgramInfoCache(@ullable ProgramList.Filter filter)59     ProgramInfoCache(@Nullable ProgramList.Filter filter) {
60         mFilter = filter;
61     }
62 
63     // Constructor for testing.
64     @VisibleForTesting
ProgramInfoCache(@ullable ProgramList.Filter filter, boolean complete, RadioManager.ProgramInfo... programInfos)65     ProgramInfoCache(@Nullable ProgramList.Filter filter, boolean complete,
66             RadioManager.ProgramInfo... programInfos) {
67         mFilter = filter;
68         mComplete = complete;
69         for (RadioManager.ProgramInfo programInfo : programInfos) {
70             mProgramInfoMap.put(programInfo.getSelector().getPrimaryId(), programInfo);
71         }
72     }
73 
74     @VisibleForTesting
programInfosAreExactly(RadioManager.ProgramInfo... programInfos)75     boolean programInfosAreExactly(RadioManager.ProgramInfo... programInfos) {
76         Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> expectedMap = new HashMap<>();
77         for (RadioManager.ProgramInfo programInfo : programInfos) {
78             expectedMap.put(programInfo.getSelector().getPrimaryId(), programInfo);
79         }
80         return expectedMap.equals(mProgramInfoMap);
81     }
82 
83     @Override
toString()84     public String toString() {
85         StringBuilder sb = new StringBuilder("ProgramInfoCache(mComplete = ");
86         sb.append(mComplete);
87         sb.append(", mFilter = ");
88         sb.append(mFilter);
89         sb.append(", mProgramInfoMap = [");
90         mProgramInfoMap.forEach((id, programInfo) -> {
91             sb.append("\n");
92             sb.append(programInfo.toString());
93         });
94         sb.append("]");
95         return sb.toString();
96     }
97 
isComplete()98     public boolean isComplete() {
99         return mComplete;
100     }
101 
getFilter()102     public @Nullable ProgramList.Filter getFilter() {
103         return mFilter;
104     }
105 
updateFromHalProgramListChunk( @onNull android.hardware.broadcastradio.V2_0.ProgramListChunk chunk)106     void updateFromHalProgramListChunk(
107             @NonNull android.hardware.broadcastradio.V2_0.ProgramListChunk chunk) {
108         if (chunk.purge) {
109             mProgramInfoMap.clear();
110         }
111         for (android.hardware.broadcastradio.V2_0.ProgramInfo halProgramInfo : chunk.modified) {
112             RadioManager.ProgramInfo programInfo = Convert.programInfoFromHal(halProgramInfo);
113             mProgramInfoMap.put(programInfo.getSelector().getPrimaryId(), programInfo);
114         }
115         for (android.hardware.broadcastradio.V2_0.ProgramIdentifier halProgramId : chunk.removed) {
116             mProgramInfoMap.remove(Convert.programIdentifierFromHal(halProgramId));
117         }
118         mComplete = chunk.complete;
119     }
120 
filterAndUpdateFrom(@onNull ProgramInfoCache other, boolean purge)121     @NonNull List<ProgramList.Chunk> filterAndUpdateFrom(@NonNull ProgramInfoCache other,
122             boolean purge) {
123         return filterAndUpdateFromInternal(other, purge, MAX_NUM_MODIFIED_PER_CHUNK,
124                 MAX_NUM_REMOVED_PER_CHUNK);
125     }
126 
127     @VisibleForTesting
filterAndUpdateFromInternal(@onNull ProgramInfoCache other, boolean purge, int maxNumModifiedPerChunk, int maxNumRemovedPerChunk)128     @NonNull List<ProgramList.Chunk> filterAndUpdateFromInternal(@NonNull ProgramInfoCache other,
129             boolean purge, int maxNumModifiedPerChunk, int maxNumRemovedPerChunk) {
130         if (purge) {
131             mProgramInfoMap.clear();
132         }
133         // If mProgramInfoMap is empty, we treat this update as a purge because this might be the
134         // first update to an AIDL client that changed its filter.
135         if (mProgramInfoMap.isEmpty()) {
136             purge = true;
137         }
138 
139         Set<RadioManager.ProgramInfo> modified = new HashSet<>();
140         Set<ProgramSelector.Identifier> removed = new HashSet<>(mProgramInfoMap.keySet());
141         for (Map.Entry<ProgramSelector.Identifier, RadioManager.ProgramInfo> entry
142                 : other.mProgramInfoMap.entrySet()) {
143             ProgramSelector.Identifier id = entry.getKey();
144             if (!passesFilter(id)) {
145                 continue;
146             }
147             removed.remove(id);
148 
149             RadioManager.ProgramInfo newInfo = entry.getValue();
150             if (!shouldIncludeInModified(newInfo)) {
151                 continue;
152             }
153             mProgramInfoMap.put(id, newInfo);
154             modified.add(newInfo);
155         }
156         for (ProgramSelector.Identifier rem : removed) {
157             mProgramInfoMap.remove(rem);
158         }
159         mComplete = other.mComplete;
160         return buildChunks(purge, mComplete, modified, maxNumModifiedPerChunk, removed,
161                 maxNumRemovedPerChunk);
162     }
163 
filterAndApplyChunk(@onNull ProgramList.Chunk chunk)164     @Nullable List<ProgramList.Chunk> filterAndApplyChunk(@NonNull ProgramList.Chunk chunk) {
165         return filterAndApplyChunkInternal(chunk, MAX_NUM_MODIFIED_PER_CHUNK,
166                 MAX_NUM_REMOVED_PER_CHUNK);
167     }
168 
169     @VisibleForTesting
filterAndApplyChunkInternal(@onNull ProgramList.Chunk chunk, int maxNumModifiedPerChunk, int maxNumRemovedPerChunk)170     @Nullable List<ProgramList.Chunk> filterAndApplyChunkInternal(@NonNull ProgramList.Chunk chunk,
171             int maxNumModifiedPerChunk, int maxNumRemovedPerChunk) {
172         if (chunk.isPurge()) {
173             mProgramInfoMap.clear();
174         }
175 
176         Set<RadioManager.ProgramInfo> modified = new HashSet<>();
177         Set<ProgramSelector.Identifier> removed = new HashSet<>();
178         for (RadioManager.ProgramInfo info : chunk.getModified()) {
179             ProgramSelector.Identifier id = info.getSelector().getPrimaryId();
180             if (!passesFilter(id) || !shouldIncludeInModified(info)) {
181                 continue;
182             }
183             mProgramInfoMap.put(id, info);
184             modified.add(info);
185         }
186         for (ProgramSelector.Identifier id : chunk.getRemoved()) {
187             if (mProgramInfoMap.containsKey(id)) {
188                 mProgramInfoMap.remove(id);
189                 removed.add(id);
190             }
191         }
192         if (modified.isEmpty() && removed.isEmpty() && mComplete == chunk.isComplete()
193                 && !chunk.isPurge()) {
194             return null;
195         }
196         mComplete = chunk.isComplete();
197         return buildChunks(chunk.isPurge(), mComplete, modified, maxNumModifiedPerChunk, removed,
198                 maxNumRemovedPerChunk);
199     }
200 
passesFilter(ProgramSelector.Identifier id)201     private boolean passesFilter(ProgramSelector.Identifier id) {
202         if (mFilter == null) {
203             return true;
204         }
205         if (!mFilter.getIdentifierTypes().isEmpty()
206                 && !mFilter.getIdentifierTypes().contains(id.getType())) {
207             return false;
208         }
209         if (!mFilter.getIdentifiers().isEmpty() && !mFilter.getIdentifiers().contains(id)) {
210             return false;
211         }
212         if (!mFilter.areCategoriesIncluded() && id.isCategoryType()) {
213             return false;
214         }
215         return true;
216     }
217 
shouldIncludeInModified(RadioManager.ProgramInfo newInfo)218     private boolean shouldIncludeInModified(RadioManager.ProgramInfo newInfo) {
219         RadioManager.ProgramInfo oldInfo = mProgramInfoMap.get(
220                 newInfo.getSelector().getPrimaryId());
221         if (oldInfo == null) {
222             return true;
223         }
224         if (mFilter != null && mFilter.areModificationsExcluded()) {
225             return false;
226         }
227         return !oldInfo.equals(newInfo);
228     }
229 
roundUpFraction(int numerator, int denominator)230     private static int roundUpFraction(int numerator, int denominator) {
231         return (numerator / denominator) + (numerator % denominator > 0 ? 1 : 0);
232     }
233 
buildChunks(boolean purge, boolean complete, @Nullable Collection<RadioManager.ProgramInfo> modified, int maxNumModifiedPerChunk, @Nullable Collection<ProgramSelector.Identifier> removed, int maxNumRemovedPerChunk)234     private static @NonNull List<ProgramList.Chunk> buildChunks(boolean purge, boolean complete,
235             @Nullable Collection<RadioManager.ProgramInfo> modified, int maxNumModifiedPerChunk,
236             @Nullable Collection<ProgramSelector.Identifier> removed, int maxNumRemovedPerChunk) {
237         // Communication protocol requires that if purge is set, removed is empty.
238         if (purge) {
239             removed = null;
240         }
241 
242         // Determine number of chunks we need to send.
243         int numChunks = purge ? 1 : 0;
244         if (modified != null) {
245             numChunks = Math.max(numChunks,
246                     roundUpFraction(modified.size(), maxNumModifiedPerChunk));
247         }
248         if (removed != null) {
249             numChunks = Math.max(numChunks, roundUpFraction(removed.size(), maxNumRemovedPerChunk));
250         }
251         if (numChunks == 0) {
252             return new ArrayList<ProgramList.Chunk>();
253         }
254 
255         // Try to make similarly-sized chunks by evenly distributing elements from modified and
256         // removed among them.
257         int modifiedPerChunk = 0;
258         int removedPerChunk = 0;
259         Iterator<RadioManager.ProgramInfo> modifiedIter = null;
260         Iterator<ProgramSelector.Identifier> removedIter = null;
261         if (modified != null) {
262             modifiedPerChunk = roundUpFraction(modified.size(), numChunks);
263             modifiedIter = modified.iterator();
264         }
265         if (removed != null) {
266             removedPerChunk = roundUpFraction(removed.size(), numChunks);
267             removedIter = removed.iterator();
268         }
269         List<ProgramList.Chunk> chunks = new ArrayList<ProgramList.Chunk>(numChunks);
270         for (int i = 0; i < numChunks; i++) {
271             HashSet<RadioManager.ProgramInfo> modifiedChunk = new HashSet<>();
272             HashSet<ProgramSelector.Identifier> removedChunk = new HashSet<>();
273             if (modifiedIter != null) {
274                 for (int j = 0; j < modifiedPerChunk && modifiedIter.hasNext(); j++) {
275                     modifiedChunk.add(modifiedIter.next());
276                 }
277             }
278             if (removedIter != null) {
279                 for (int j = 0; j < removedPerChunk && removedIter.hasNext(); j++) {
280                     removedChunk.add(removedIter.next());
281                 }
282             }
283             chunks.add(new ProgramList.Chunk(purge && i == 0, complete && (i == numChunks - 1),
284                       modifiedChunk, removedChunk));
285         }
286         return chunks;
287     }
288 }
289