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