blob: 8c938910114176614d16d3f870e23ea280a1a59e [file] [log] [blame]
/**
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.broadcastradio.hal2;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.hardware.radio.ProgramList;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
class ProgramInfoCache {
// Maximum number of RadioManager.ProgramInfo elements that will be put into a
// ProgramList.Chunk.mModified array. Used to try to ensure a single ProgramList.Chunk stays
// within the AIDL data size limit.
private static final int MAX_NUM_MODIFIED_PER_CHUNK = 100;
// Maximum number of ProgramSelector.Identifier elements that will be put into a
// ProgramList.Chunk.mRemoved array. Used to try to ensure a single ProgramList.Chunk stays
// within the AIDL data size limit.
private static final int MAX_NUM_REMOVED_PER_CHUNK = 500;
// Map from primary identifier to corresponding ProgramInfo.
private final Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> mProgramInfoMap =
new HashMap<>();
// Flag indicating whether mProgramInfoMap is considered complete based upon the received
// updates.
private boolean mComplete = true;
// Optional filter used in filterAndUpdateFrom(). Usually this field is null for a HAL-side
// cache and non-null for an AIDL-side cache.
private final ProgramList.Filter mFilter;
ProgramInfoCache(@Nullable ProgramList.Filter filter) {
mFilter = filter;
}
// Constructor for testing.
@VisibleForTesting
ProgramInfoCache(@Nullable ProgramList.Filter filter, boolean complete,
RadioManager.ProgramInfo... programInfos) {
mFilter = filter;
mComplete = complete;
for (RadioManager.ProgramInfo programInfo : programInfos) {
mProgramInfoMap.put(programInfo.getSelector().getPrimaryId(), programInfo);
}
}
@VisibleForTesting
boolean programInfosAreExactly(RadioManager.ProgramInfo... programInfos) {
Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> expectedMap = new HashMap<>();
for (RadioManager.ProgramInfo programInfo : programInfos) {
expectedMap.put(programInfo.getSelector().getPrimaryId(), programInfo);
}
return expectedMap.equals(mProgramInfoMap);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("ProgramInfoCache(mComplete = ");
sb.append(mComplete);
sb.append(", mFilter = ");
sb.append(mFilter);
sb.append(", mProgramInfoMap = [");
mProgramInfoMap.forEach((id, programInfo) -> {
sb.append("\n");
sb.append(programInfo.toString());
});
sb.append("]");
return sb.toString();
}
public boolean isComplete() {
return mComplete;
}
public @Nullable ProgramList.Filter getFilter() {
return mFilter;
}
void updateFromHalProgramListChunk(
@NonNull android.hardware.broadcastradio.V2_0.ProgramListChunk chunk) {
if (chunk.purge) {
mProgramInfoMap.clear();
}
for (android.hardware.broadcastradio.V2_0.ProgramInfo halProgramInfo : chunk.modified) {
RadioManager.ProgramInfo programInfo = Convert.programInfoFromHal(halProgramInfo);
mProgramInfoMap.put(programInfo.getSelector().getPrimaryId(), programInfo);
}
for (android.hardware.broadcastradio.V2_0.ProgramIdentifier halProgramId : chunk.removed) {
mProgramInfoMap.remove(Convert.programIdentifierFromHal(halProgramId));
}
mComplete = chunk.complete;
}
@NonNull List<ProgramList.Chunk> filterAndUpdateFrom(@NonNull ProgramInfoCache other,
boolean purge) {
return filterAndUpdateFromInternal(other, purge, MAX_NUM_MODIFIED_PER_CHUNK,
MAX_NUM_REMOVED_PER_CHUNK);
}
@VisibleForTesting
@NonNull List<ProgramList.Chunk> filterAndUpdateFromInternal(@NonNull ProgramInfoCache other,
boolean purge, int maxNumModifiedPerChunk, int maxNumRemovedPerChunk) {
if (purge) {
mProgramInfoMap.clear();
}
// If mProgramInfoMap is empty, we treat this update as a purge because this might be the
// first update to an AIDL client that changed its filter.
if (mProgramInfoMap.isEmpty()) {
purge = true;
}
Set<RadioManager.ProgramInfo> modified = new HashSet<>();
Set<ProgramSelector.Identifier> removed = new HashSet<>(mProgramInfoMap.keySet());
for (Map.Entry<ProgramSelector.Identifier, RadioManager.ProgramInfo> entry
: other.mProgramInfoMap.entrySet()) {
ProgramSelector.Identifier id = entry.getKey();
if (!passesFilter(id)) {
continue;
}
removed.remove(id);
RadioManager.ProgramInfo newInfo = entry.getValue();
if (!shouldIncludeInModified(newInfo)) {
continue;
}
mProgramInfoMap.put(id, newInfo);
modified.add(newInfo);
}
for (ProgramSelector.Identifier rem : removed) {
mProgramInfoMap.remove(rem);
}
mComplete = other.mComplete;
return buildChunks(purge, mComplete, modified, maxNumModifiedPerChunk, removed,
maxNumRemovedPerChunk);
}
@Nullable List<ProgramList.Chunk> filterAndApplyChunk(@NonNull ProgramList.Chunk chunk) {
return filterAndApplyChunkInternal(chunk, MAX_NUM_MODIFIED_PER_CHUNK,
MAX_NUM_REMOVED_PER_CHUNK);
}
@VisibleForTesting
@Nullable List<ProgramList.Chunk> filterAndApplyChunkInternal(@NonNull ProgramList.Chunk chunk,
int maxNumModifiedPerChunk, int maxNumRemovedPerChunk) {
if (chunk.isPurge()) {
mProgramInfoMap.clear();
}
Set<RadioManager.ProgramInfo> modified = new HashSet<>();
Set<ProgramSelector.Identifier> removed = new HashSet<>();
for (RadioManager.ProgramInfo info : chunk.getModified()) {
ProgramSelector.Identifier id = info.getSelector().getPrimaryId();
if (!passesFilter(id) || !shouldIncludeInModified(info)) {
continue;
}
mProgramInfoMap.put(id, info);
modified.add(info);
}
for (ProgramSelector.Identifier id : chunk.getRemoved()) {
if (mProgramInfoMap.containsKey(id)) {
mProgramInfoMap.remove(id);
removed.add(id);
}
}
if (modified.isEmpty() && removed.isEmpty() && mComplete == chunk.isComplete()) {
return null;
}
mComplete = chunk.isComplete();
return buildChunks(chunk.isPurge(), mComplete, modified, maxNumModifiedPerChunk, removed,
maxNumRemovedPerChunk);
}
private boolean passesFilter(ProgramSelector.Identifier id) {
if (mFilter == null) {
return true;
}
if (!mFilter.getIdentifierTypes().isEmpty()
&& !mFilter.getIdentifierTypes().contains(id.getType())) {
return false;
}
if (!mFilter.getIdentifiers().isEmpty() && !mFilter.getIdentifiers().contains(id)) {
return false;
}
if (!mFilter.areCategoriesIncluded() && id.isCategoryType()) {
return false;
}
return true;
}
private boolean shouldIncludeInModified(RadioManager.ProgramInfo newInfo) {
RadioManager.ProgramInfo oldInfo = mProgramInfoMap.get(
newInfo.getSelector().getPrimaryId());
if (oldInfo == null) {
return true;
}
if (mFilter != null && mFilter.areModificationsExcluded()) {
return false;
}
return !oldInfo.equals(newInfo);
}
private static int roundUpFraction(int numerator, int denominator) {
return (numerator / denominator) + (numerator % denominator > 0 ? 1 : 0);
}
private static @NonNull List<ProgramList.Chunk> buildChunks(boolean purge, boolean complete,
@Nullable Collection<RadioManager.ProgramInfo> modified, int maxNumModifiedPerChunk,
@Nullable Collection<ProgramSelector.Identifier> removed, int maxNumRemovedPerChunk) {
// Communication protocol requires that if purge is set, removed is empty.
if (purge) {
removed = null;
}
// Determine number of chunks we need to send.
int numChunks = 0;
if (modified != null) {
numChunks = roundUpFraction(modified.size(), maxNumModifiedPerChunk);
}
if (removed != null) {
numChunks = Math.max(numChunks, roundUpFraction(removed.size(), maxNumRemovedPerChunk));
}
if (numChunks == 0) {
return new ArrayList<ProgramList.Chunk>();
}
// Try to make similarly-sized chunks by evenly distributing elements from modified and
// removed among them.
int modifiedPerChunk = 0;
int removedPerChunk = 0;
Iterator<RadioManager.ProgramInfo> modifiedIter = null;
Iterator<ProgramSelector.Identifier> removedIter = null;
if (modified != null) {
modifiedPerChunk = roundUpFraction(modified.size(), numChunks);
modifiedIter = modified.iterator();
}
if (removed != null) {
removedPerChunk = roundUpFraction(removed.size(), numChunks);
removedIter = removed.iterator();
}
List<ProgramList.Chunk> chunks = new ArrayList<ProgramList.Chunk>(numChunks);
for (int i = 0; i < numChunks; i++) {
HashSet<RadioManager.ProgramInfo> modifiedChunk = new HashSet<>();
HashSet<ProgramSelector.Identifier> removedChunk = new HashSet<>();
if (modifiedIter != null) {
for (int j = 0; j < modifiedPerChunk && modifiedIter.hasNext(); j++) {
modifiedChunk.add(modifiedIter.next());
}
}
if (removedIter != null) {
for (int j = 0; j < removedPerChunk && removedIter.hasNext(); j++) {
removedChunk.add(removedIter.next());
}
}
chunks.add(new ProgramList.Chunk(purge && i == 0, complete && (i == numChunks - 1),
modifiedChunk, removedChunk));
}
return chunks;
}
}