Merge "Rewire ProgramInfo updates for 1 HAL : N AIDL fanout"
This commit is contained in:
@@ -369,6 +369,33 @@ public final class ProgramList implements AutoCloseable {
|
||||
public boolean areModificationsExcluded() {
|
||||
return mExcludeModifications;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(mIdentifierTypes, mIdentifiers, mIncludeCategories,
|
||||
mExcludeModifications);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Filter)) return false;
|
||||
Filter other = (Filter) obj;
|
||||
|
||||
if (mIncludeCategories != other.mIncludeCategories) return false;
|
||||
if (mExcludeModifications != other.mExcludeModifications) return false;
|
||||
if (!Objects.equals(mIdentifierTypes, other.mIdentifierTypes)) return false;
|
||||
if (!Objects.equals(mIdentifiers, other.mIdentifiers)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Filter [mIdentifierTypes=" + mIdentifierTypes
|
||||
+ ", mIdentifiers=" + mIdentifiers
|
||||
+ ", mIncludeCategories=" + mIncludeCategories
|
||||
+ ", mExcludeModifications=" + mExcludeModifications + "]";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -436,5 +463,24 @@ public final class ProgramList implements AutoCloseable {
|
||||
public @NonNull Set<ProgramSelector.Identifier> getRemoved() {
|
||||
return mRemoved;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Chunk)) return false;
|
||||
Chunk other = (Chunk) obj;
|
||||
|
||||
if (mPurge != other.mPurge) return false;
|
||||
if (mComplete != other.mComplete) return false;
|
||||
if (!Objects.equals(mModified, other.mModified)) return false;
|
||||
if (!Objects.equals(mRemoved, other.mRemoved)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Chunk [mPurge=" + mPurge + ", mComplete=" + mComplete
|
||||
+ ", mModified=" + mModified + ", mRemoved=" + mRemoved + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +257,11 @@ public final class RadioMetadata implements Parcelable {
|
||||
|
||||
private final Bundle mBundle;
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mBundle.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
|
||||
@@ -17,12 +17,10 @@ package com.android.server.broadcastradio.hal2;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import android.hardware.broadcastradio.V2_0.ProgramInfo;
|
||||
import android.hardware.broadcastradio.V2_0.ProgramListChunk;
|
||||
import android.hardware.radio.ProgramList;
|
||||
import android.hardware.radio.ProgramSelector;
|
||||
import android.hardware.radio.RadioManager;
|
||||
import android.hardware.radio.RadioMetadata;
|
||||
import android.test.suitebuilder.annotation.MediumTest;
|
||||
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
@@ -30,7 +28,6 @@ import androidx.test.runner.AndroidJUnit4;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -45,55 +42,58 @@ public class ProgramInfoCacheTest {
|
||||
|
||||
private final ProgramSelector.Identifier mAmFmIdentifier =
|
||||
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, 88500);
|
||||
private final RadioManager.ProgramInfo mAmFmInfo = makeProgramInfo(
|
||||
private final RadioManager.ProgramInfo mAmFmInfo = TestUtils.makeProgramInfo(
|
||||
ProgramSelector.PROGRAM_TYPE_FM, mAmFmIdentifier, 0);
|
||||
|
||||
private final ProgramSelector.Identifier mRdsIdentifier =
|
||||
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_RDS_PI, 15019);
|
||||
private final RadioManager.ProgramInfo mRdsInfo = makeProgramInfo(
|
||||
private final RadioManager.ProgramInfo mRdsInfo = TestUtils.makeProgramInfo(
|
||||
ProgramSelector.PROGRAM_TYPE_FM, mRdsIdentifier, 0);
|
||||
|
||||
private final ProgramSelector.Identifier mDabEnsembleIdentifier =
|
||||
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, 1337);
|
||||
private final RadioManager.ProgramInfo mDabEnsembleInfo = makeProgramInfo(
|
||||
private final RadioManager.ProgramInfo mDabEnsembleInfo = TestUtils.makeProgramInfo(
|
||||
ProgramSelector.PROGRAM_TYPE_DAB, mDabEnsembleIdentifier, 0);
|
||||
|
||||
private final ProgramSelector.Identifier mVendorCustomIdentifier =
|
||||
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_VENDOR_START, 9001);
|
||||
private final RadioManager.ProgramInfo mVendorCustomInfo = makeProgramInfo(
|
||||
private final RadioManager.ProgramInfo mVendorCustomInfo = TestUtils.makeProgramInfo(
|
||||
ProgramSelector.PROGRAM_TYPE_VENDOR_START, mVendorCustomIdentifier, 0);
|
||||
|
||||
// HAL-side ProgramInfoCache containing all of the above ProgramInfos.
|
||||
private final ProgramInfoCache mAllProgramInfos = new ProgramInfoCache(null, mAmFmInfo,
|
||||
private final ProgramInfoCache mAllProgramInfos = new ProgramInfoCache(null, true, mAmFmInfo,
|
||||
mRdsInfo, mDabEnsembleInfo, mVendorCustomInfo);
|
||||
|
||||
@Test
|
||||
public void testUpdateFromHal() {
|
||||
// First test a purging chunk.
|
||||
ProgramInfoCache cache = new ProgramInfoCache(null, mAmFmInfo);
|
||||
// First test updating an incomplete cache with a purging, complete chunk.
|
||||
ProgramInfoCache cache = new ProgramInfoCache(null, false, mAmFmInfo);
|
||||
ProgramListChunk chunk = new ProgramListChunk();
|
||||
chunk.purge = true;
|
||||
chunk.modified.add(programInfoToHal(mRdsInfo));
|
||||
chunk.modified.add(programInfoToHal(mDabEnsembleInfo));
|
||||
cache.updateFromHalProgramListChunk(chunk);
|
||||
chunk.complete = true;
|
||||
chunk.modified.add(TestUtils.programInfoToHal(mRdsInfo));
|
||||
chunk.modified.add(TestUtils.programInfoToHal(mDabEnsembleInfo));
|
||||
cache.updateFromHalProgramListChunk(chunk);
|
||||
assertTrue(cache.programInfosAreExactly(mRdsInfo, mDabEnsembleInfo));
|
||||
assertTrue(cache.isComplete());
|
||||
|
||||
// Then test a non-purging chunk.
|
||||
// Then test a non-purging, incomplete chunk.
|
||||
chunk.purge = false;
|
||||
chunk.complete = false;
|
||||
chunk.modified.clear();
|
||||
RadioManager.ProgramInfo updatedRdsInfo = makeProgramInfo(ProgramSelector.PROGRAM_TYPE_FM,
|
||||
mRdsIdentifier, 1);
|
||||
chunk.modified.add(programInfoToHal(updatedRdsInfo));
|
||||
chunk.modified.add(programInfoToHal(mVendorCustomInfo));
|
||||
RadioManager.ProgramInfo updatedRdsInfo = TestUtils.makeProgramInfo(
|
||||
ProgramSelector.PROGRAM_TYPE_FM, mRdsIdentifier, 1);
|
||||
chunk.modified.add(TestUtils.programInfoToHal(updatedRdsInfo));
|
||||
chunk.modified.add(TestUtils.programInfoToHal(mVendorCustomInfo));
|
||||
chunk.removed.add(Convert.programIdentifierToHal(mDabEnsembleIdentifier));
|
||||
cache.updateFromHalProgramListChunk(chunk);
|
||||
assertTrue(cache.programInfosAreExactly(updatedRdsInfo, mVendorCustomInfo));
|
||||
assertFalse(cache.isComplete());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullFilter() {
|
||||
ProgramInfoCache cache = new ProgramInfoCache(null);
|
||||
ProgramInfoCache cache = new ProgramInfoCache(null, true);
|
||||
cache.filterAndUpdateFrom(mAllProgramInfos, false);
|
||||
assertTrue(cache.programInfosAreExactly(mAmFmInfo, mRdsInfo, mDabEnsembleInfo,
|
||||
mVendorCustomInfo));
|
||||
@@ -140,11 +140,11 @@ public class ProgramInfoCacheTest {
|
||||
|
||||
@Test
|
||||
public void testPurgeUpdateChunks() {
|
||||
ProgramInfoCache cache = new ProgramInfoCache(null, mAmFmInfo);
|
||||
ProgramInfoCache cache = new ProgramInfoCache(null, false, mAmFmInfo);
|
||||
List<ProgramList.Chunk> chunks =
|
||||
cache.filterAndUpdateFromInternal(mAllProgramInfos, true, 3, 3);
|
||||
assertEquals(2, chunks.size());
|
||||
verifyChunkListFlags(chunks, true);
|
||||
verifyChunkListFlags(chunks, true, true);
|
||||
verifyChunkListModified(chunks, 3, mAmFmInfo, mRdsInfo, mDabEnsembleInfo,
|
||||
mVendorCustomInfo);
|
||||
verifyChunkListRemoved(chunks, 0);
|
||||
@@ -154,23 +154,26 @@ public class ProgramInfoCacheTest {
|
||||
public void testDeltaUpdateChunksModificationsIncluded() {
|
||||
// Create a cache with a filter that allows modifications, and set its contents to
|
||||
// mAmFmInfo, mRdsInfo, mDabEnsembleInfo, and mVendorCustomInfo.
|
||||
ProgramInfoCache cache = new ProgramInfoCache(null, mAmFmInfo, mRdsInfo, mDabEnsembleInfo,
|
||||
mVendorCustomInfo);
|
||||
ProgramInfoCache cache = new ProgramInfoCache(null, true, mAmFmInfo, mRdsInfo,
|
||||
mDabEnsembleInfo, mVendorCustomInfo);
|
||||
|
||||
// Create a HAL cache that:
|
||||
// - Is complete.
|
||||
// - Retains mAmFmInfo.
|
||||
// - Replaces mRdsInfo with updatedRdsInfo.
|
||||
// - Drops mDabEnsembleInfo and mVendorCustomInfo.
|
||||
// - Introduces a new SXM info.
|
||||
RadioManager.ProgramInfo updatedRdsInfo = makeProgramInfo(ProgramSelector.PROGRAM_TYPE_FM,
|
||||
mRdsIdentifier, 1);
|
||||
RadioManager.ProgramInfo newSxmInfo = makeProgramInfo(ProgramSelector.PROGRAM_TYPE_SXM,
|
||||
RadioManager.ProgramInfo updatedRdsInfo = TestUtils.makeProgramInfo(
|
||||
ProgramSelector.PROGRAM_TYPE_FM, mRdsIdentifier, 1);
|
||||
RadioManager.ProgramInfo newSxmInfo = TestUtils.makeProgramInfo(
|
||||
ProgramSelector.PROGRAM_TYPE_SXM,
|
||||
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL, 12345),
|
||||
0);
|
||||
ProgramInfoCache halCache = new ProgramInfoCache(null, mAmFmInfo, updatedRdsInfo,
|
||||
ProgramInfoCache halCache = new ProgramInfoCache(null, true, mAmFmInfo, updatedRdsInfo,
|
||||
newSxmInfo);
|
||||
|
||||
// Update the cache and verify:
|
||||
// - The final chunk's complete flag is set.
|
||||
// - mAmFmInfo is retained and not reported in the chunks.
|
||||
// - updatedRdsInfo should appear as an update to mRdsInfo.
|
||||
// - newSxmInfo should appear as a new entry.
|
||||
@@ -178,7 +181,7 @@ public class ProgramInfoCacheTest {
|
||||
List<ProgramList.Chunk> chunks = cache.filterAndUpdateFromInternal(halCache, false, 5, 1);
|
||||
assertTrue(cache.programInfosAreExactly(mAmFmInfo, updatedRdsInfo, newSxmInfo));
|
||||
assertEquals(2, chunks.size());
|
||||
verifyChunkListFlags(chunks, false);
|
||||
verifyChunkListFlags(chunks, false, true);
|
||||
verifyChunkListModified(chunks, 5, updatedRdsInfo, newSxmInfo);
|
||||
verifyChunkListRemoved(chunks, 1, mDabEnsembleIdentifier, mVendorCustomIdentifier);
|
||||
}
|
||||
@@ -188,63 +191,50 @@ public class ProgramInfoCacheTest {
|
||||
// Create a cache with a filter that excludes modifications, and set its contents to
|
||||
// mAmFmInfo, mRdsInfo, mDabEnsembleInfo, and mVendorCustomInfo.
|
||||
ProgramInfoCache cache = new ProgramInfoCache(new ProgramList.Filter(new HashSet<Integer>(),
|
||||
new HashSet<ProgramSelector.Identifier>(), true, true), mAmFmInfo, mRdsInfo,
|
||||
new HashSet<ProgramSelector.Identifier>(), true, true), true, mAmFmInfo, mRdsInfo,
|
||||
mDabEnsembleInfo, mVendorCustomInfo);
|
||||
|
||||
// Create a HAL cache that:
|
||||
// - Is incomplete.
|
||||
// - Retains mAmFmInfo.
|
||||
// - Replaces mRdsInfo with updatedRdsInfo.
|
||||
// - Drops mDabEnsembleInfo and mVendorCustomInfo.
|
||||
// - Introduces a new SXM info.
|
||||
RadioManager.ProgramInfo updatedRdsInfo = makeProgramInfo(ProgramSelector.PROGRAM_TYPE_FM,
|
||||
mRdsIdentifier, 1);
|
||||
RadioManager.ProgramInfo newSxmInfo = makeProgramInfo(ProgramSelector.PROGRAM_TYPE_SXM,
|
||||
RadioManager.ProgramInfo updatedRdsInfo = TestUtils.makeProgramInfo(
|
||||
ProgramSelector.PROGRAM_TYPE_FM, mRdsIdentifier, 1);
|
||||
RadioManager.ProgramInfo newSxmInfo = TestUtils.makeProgramInfo(
|
||||
ProgramSelector.PROGRAM_TYPE_SXM,
|
||||
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL, 12345),
|
||||
0);
|
||||
ProgramInfoCache halCache = new ProgramInfoCache(null, mAmFmInfo, updatedRdsInfo,
|
||||
ProgramInfoCache halCache = new ProgramInfoCache(null, false, mAmFmInfo, updatedRdsInfo,
|
||||
newSxmInfo);
|
||||
|
||||
// Update the cache and verify:
|
||||
// - All complete flags are false.
|
||||
// - mAmFmInfo and mRdsInfo are retained and not reported in the chunks.
|
||||
// - newSxmInfo should appear as a new entry.
|
||||
// - mDabEnsembleInfo and mVendorCustomInfo should be reported as removed.
|
||||
List<ProgramList.Chunk> chunks = cache.filterAndUpdateFromInternal(halCache, false, 5, 1);
|
||||
assertTrue(cache.programInfosAreExactly(mAmFmInfo, mRdsInfo, newSxmInfo));
|
||||
assertEquals(2, chunks.size());
|
||||
verifyChunkListFlags(chunks, false);
|
||||
verifyChunkListFlags(chunks, false, false);
|
||||
verifyChunkListModified(chunks, 5, newSxmInfo);
|
||||
verifyChunkListRemoved(chunks, 1, mDabEnsembleIdentifier, mVendorCustomIdentifier);
|
||||
}
|
||||
|
||||
private static RadioManager.ProgramInfo makeProgramInfo(int programType,
|
||||
ProgramSelector.Identifier identifier, int signalQuality) {
|
||||
// Note: If you set new fields, check if programInfoToHal() needs to be updated as well.
|
||||
return new RadioManager.ProgramInfo(new ProgramSelector(programType, identifier, null,
|
||||
null), null, null, null, 0, signalQuality, new RadioMetadata.Builder().build(),
|
||||
new HashMap<String, String>());
|
||||
}
|
||||
|
||||
private static ProgramInfo programInfoToHal(RadioManager.ProgramInfo info) {
|
||||
// Note that because Convert does not by design provide functions for all conversions, this
|
||||
// function only copies fields that are set by makeProgramInfo().
|
||||
ProgramInfo hwInfo = new ProgramInfo();
|
||||
hwInfo.selector = Convert.programSelectorToHal(info.getSelector());
|
||||
hwInfo.signalQuality = info.getSignalStrength();
|
||||
return hwInfo;
|
||||
}
|
||||
|
||||
// Verifies that:
|
||||
// - The first chunk's purge flag matches expectPurge.
|
||||
// - The last chunk's complete flag is set.
|
||||
// - The last chunk's complete flag matches expectComplete.
|
||||
// - All other flags are false.
|
||||
private static void verifyChunkListFlags(List<ProgramList.Chunk> chunks, boolean expectPurge) {
|
||||
private static void verifyChunkListFlags(List<ProgramList.Chunk> chunks, boolean expectPurge,
|
||||
boolean expectComplete) {
|
||||
if (chunks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < chunks.size(); i++) {
|
||||
ProgramList.Chunk chunk = chunks.get(i);
|
||||
assertEquals(i == 0 && expectPurge, chunk.isPurge());
|
||||
assertEquals(i == chunks.size() - 1, chunk.isComplete());
|
||||
assertEquals(i == chunks.size() - 1 && expectComplete, chunk.isComplete());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
* 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 static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.argThat;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.hardware.broadcastradio.V2_0.IBroadcastRadio;
|
||||
import android.hardware.broadcastradio.V2_0.ITunerCallback;
|
||||
import android.hardware.broadcastradio.V2_0.ITunerSession;
|
||||
import android.hardware.broadcastradio.V2_0.ProgramFilter;
|
||||
import android.hardware.broadcastradio.V2_0.ProgramListChunk;
|
||||
import android.hardware.broadcastradio.V2_0.Result;
|
||||
import android.hardware.radio.ProgramList;
|
||||
import android.hardware.radio.ProgramSelector;
|
||||
import android.hardware.radio.RadioManager;
|
||||
import android.os.RemoteException;
|
||||
import android.test.suitebuilder.annotation.MediumTest;
|
||||
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentMatcher;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Tests for v2 HAL RadioModule.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@MediumTest
|
||||
public class StartProgramListUpdatesFanoutTest {
|
||||
private static final String TAG = "BroadcastRadioTests.hal2.StartProgramListUpdatesFanout";
|
||||
|
||||
// Mocks
|
||||
@Mock IBroadcastRadio mBroadcastRadioMock;
|
||||
@Mock ITunerSession mHalTunerSessionMock;
|
||||
private android.hardware.radio.ITunerCallback[] mAidlTunerCallbackMocks;
|
||||
|
||||
// RadioModule under test
|
||||
private RadioModule mRadioModule;
|
||||
|
||||
// Objects created by mRadioModule
|
||||
private ITunerCallback mHalTunerCallback;
|
||||
private TunerSession[] mTunerSessions;
|
||||
|
||||
// Data objects used during tests
|
||||
private final ProgramSelector.Identifier mAmFmIdentifier =
|
||||
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, 88500);
|
||||
private final RadioManager.ProgramInfo mAmFmInfo = TestUtils.makeProgramInfo(
|
||||
ProgramSelector.PROGRAM_TYPE_FM, mAmFmIdentifier, 0);
|
||||
private final RadioManager.ProgramInfo mModifiedAmFmInfo = TestUtils.makeProgramInfo(
|
||||
ProgramSelector.PROGRAM_TYPE_FM, mAmFmIdentifier, 1);
|
||||
|
||||
private final ProgramSelector.Identifier mRdsIdentifier =
|
||||
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_RDS_PI, 15019);
|
||||
private final RadioManager.ProgramInfo mRdsInfo = TestUtils.makeProgramInfo(
|
||||
ProgramSelector.PROGRAM_TYPE_FM, mRdsIdentifier, 0);
|
||||
|
||||
private final ProgramSelector.Identifier mDabEnsembleIdentifier =
|
||||
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, 1337);
|
||||
private final RadioManager.ProgramInfo mDabEnsembleInfo = TestUtils.makeProgramInfo(
|
||||
ProgramSelector.PROGRAM_TYPE_DAB, mDabEnsembleIdentifier, 0);
|
||||
|
||||
@Before
|
||||
public void setup() throws RemoteException {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
mRadioModule = new RadioModule(mBroadcastRadioMock, new RadioManager.ModuleProperties(0, "",
|
||||
0, "", "", "", "", 0, 0, false, false, null, false, new int[] {}, new int[] {},
|
||||
null, null));
|
||||
|
||||
doAnswer((Answer) invocation -> {
|
||||
mHalTunerCallback = (ITunerCallback) invocation.getArguments()[0];
|
||||
IBroadcastRadio.openSessionCallback cb = (IBroadcastRadio.openSessionCallback)
|
||||
invocation.getArguments()[1];
|
||||
cb.onValues(Result.OK, mHalTunerSessionMock);
|
||||
return null;
|
||||
}).when(mBroadcastRadioMock).openSession(any(), any());
|
||||
when(mHalTunerSessionMock.startProgramListUpdates(any())).thenReturn(Result.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFanout() throws RemoteException {
|
||||
// Open 3 clients that will all use the same filter, and start updates on two of them for
|
||||
// now. The HAL TunerSession should only see 1 filter update.
|
||||
openAidlClients(3);
|
||||
ProgramList.Filter aidlFilter = new ProgramList.Filter(new HashSet<Integer>(),
|
||||
new HashSet<ProgramSelector.Identifier>(), true, false);
|
||||
ProgramFilter halFilter = Convert.programFilterToHal(aidlFilter);
|
||||
for (int i = 0; i < 2; i++) {
|
||||
mTunerSessions[i].startProgramListUpdates(aidlFilter);
|
||||
}
|
||||
verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
|
||||
|
||||
// Initiate a program list update from the HAL side and verify both connected AIDL clients
|
||||
// receive the update.
|
||||
updateHalProgramInfo(true, Arrays.asList(mAmFmInfo, mRdsInfo), null);
|
||||
for (int i = 0; i < 2; i++) {
|
||||
verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[i], true, Arrays.asList(
|
||||
mAmFmInfo, mRdsInfo), null);
|
||||
}
|
||||
|
||||
// Repeat with a non-purging update.
|
||||
updateHalProgramInfo(false, Arrays.asList(mModifiedAmFmInfo),
|
||||
Arrays.asList(mRdsIdentifier));
|
||||
for (int i = 0; i < 2; i++) {
|
||||
verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[i], false,
|
||||
Arrays.asList(mModifiedAmFmInfo), Arrays.asList(mRdsIdentifier));
|
||||
}
|
||||
|
||||
// Now start updates on the 3rd client. Verify the HAL function has not been called again
|
||||
// and client receives the appropriate update.
|
||||
mTunerSessions[2].startProgramListUpdates(aidlFilter);
|
||||
verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(any());
|
||||
verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[2], true,
|
||||
Arrays.asList(mModifiedAmFmInfo), null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFiltering() throws RemoteException {
|
||||
// Open 4 clients that will use the following filters:
|
||||
// [0]: ID mRdsIdentifier, modifications excluded
|
||||
// [1]: No categories, modifications excluded
|
||||
// [2]: Type IDENTIFIER_TYPE_AMFM_FREQUENCY, modifications excluded
|
||||
// [3]: Type IDENTIFIER_TYPE_AMFM_FREQUENCY, modifications included
|
||||
openAidlClients(4);
|
||||
ProgramList.Filter idFilter = new ProgramList.Filter(new HashSet<Integer>(),
|
||||
new HashSet<ProgramSelector.Identifier>(Arrays.asList(mRdsIdentifier)), true, true);
|
||||
ProgramList.Filter categoryFilter = new ProgramList.Filter(new HashSet<Integer>(),
|
||||
new HashSet<ProgramSelector.Identifier>(), false, true);
|
||||
ProgramList.Filter typeFilterWithoutModifications = new ProgramList.Filter(
|
||||
new HashSet<Integer>(Arrays.asList(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)),
|
||||
new HashSet<ProgramSelector.Identifier>(), true, true);
|
||||
ProgramList.Filter typeFilterWithModifications = new ProgramList.Filter(
|
||||
new HashSet<Integer>(Arrays.asList(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)),
|
||||
new HashSet<ProgramSelector.Identifier>(), true, false);
|
||||
|
||||
// Start updates on the clients in order. The HAL filter should get updated after each
|
||||
// client except [2].
|
||||
mTunerSessions[0].startProgramListUpdates(idFilter);
|
||||
ProgramFilter halFilter = Convert.programFilterToHal(idFilter);
|
||||
verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
|
||||
|
||||
mTunerSessions[1].startProgramListUpdates(categoryFilter);
|
||||
halFilter.identifiers.clear();
|
||||
verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
|
||||
|
||||
mTunerSessions[2].startProgramListUpdates(typeFilterWithoutModifications);
|
||||
verify(mHalTunerSessionMock, times(2)).startProgramListUpdates(any());
|
||||
|
||||
mTunerSessions[3].startProgramListUpdates(typeFilterWithModifications);
|
||||
halFilter.excludeModifications = false;
|
||||
verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
|
||||
|
||||
// Adding mRdsInfo should update clients [0] and [1].
|
||||
updateHalProgramInfo(false, Arrays.asList(mRdsInfo), null);
|
||||
verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], false, Arrays.asList(mRdsInfo),
|
||||
null);
|
||||
verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], false, Arrays.asList(mRdsInfo),
|
||||
null);
|
||||
|
||||
// Adding mAmFmInfo should update clients [1], [2], and [3].
|
||||
updateHalProgramInfo(false, Arrays.asList(mAmFmInfo), null);
|
||||
verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], false, Arrays.asList(mAmFmInfo),
|
||||
null);
|
||||
verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[2], false, Arrays.asList(mAmFmInfo),
|
||||
null);
|
||||
verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[3], false, Arrays.asList(mAmFmInfo),
|
||||
null);
|
||||
|
||||
// Modifying mAmFmInfo to mModifiedAmFmInfo should update only [3].
|
||||
updateHalProgramInfo(false, Arrays.asList(mModifiedAmFmInfo), null);
|
||||
verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[3], false,
|
||||
Arrays.asList(mModifiedAmFmInfo), null);
|
||||
|
||||
// Adding mDabEnsembleInfo should not update any client.
|
||||
updateHalProgramInfo(false, Arrays.asList(mDabEnsembleInfo), null);
|
||||
verify(mAidlTunerCallbackMocks[0], times(1)).onProgramListUpdated(any());
|
||||
verify(mAidlTunerCallbackMocks[1], times(2)).onProgramListUpdated(any());
|
||||
verify(mAidlTunerCallbackMocks[2], times(1)).onProgramListUpdated(any());
|
||||
verify(mAidlTunerCallbackMocks[3], times(2)).onProgramListUpdated(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientClosing() throws RemoteException {
|
||||
// Open 2 clients that use different filters that are both sensitive to mAmFmIdentifier.
|
||||
openAidlClients(2);
|
||||
ProgramList.Filter idFilter = new ProgramList.Filter(new HashSet<Integer>(),
|
||||
new HashSet<ProgramSelector.Identifier>(Arrays.asList(mAmFmIdentifier)), true,
|
||||
false);
|
||||
ProgramList.Filter typeFilter = new ProgramList.Filter(
|
||||
new HashSet<Integer>(Arrays.asList(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)),
|
||||
new HashSet<ProgramSelector.Identifier>(), true, false);
|
||||
|
||||
// Start updates on the clients, and verify the HAL filter is updated after each one.
|
||||
mTunerSessions[0].startProgramListUpdates(idFilter);
|
||||
ProgramFilter halFilter = Convert.programFilterToHal(idFilter);
|
||||
verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
|
||||
|
||||
mTunerSessions[1].startProgramListUpdates(typeFilter);
|
||||
halFilter.identifiers.clear();
|
||||
verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
|
||||
|
||||
// Update the HAL with mAmFmInfo, and verify both clients are updated.
|
||||
updateHalProgramInfo(true, Arrays.asList(mAmFmInfo), null);
|
||||
verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], true, Arrays.asList(mAmFmInfo),
|
||||
null);
|
||||
verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], true, Arrays.asList(mAmFmInfo),
|
||||
null);
|
||||
|
||||
// Stop updates on the first client and verify the HAL filter is updated.
|
||||
mTunerSessions[0].stopProgramListUpdates();
|
||||
verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(Convert.programFilterToHal(
|
||||
typeFilter));
|
||||
|
||||
// Update the HAL with mModifiedAmFmInfo, and verify only the remaining client is updated.
|
||||
updateHalProgramInfo(true, Arrays.asList(mModifiedAmFmInfo), null);
|
||||
verify(mAidlTunerCallbackMocks[0], times(1)).onProgramListUpdated(any());
|
||||
verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], true,
|
||||
Arrays.asList(mModifiedAmFmInfo), null);
|
||||
|
||||
// Close the other client without explicitly stopping updates, and verify HAL updates are
|
||||
// stopped as well.
|
||||
mTunerSessions[1].close();
|
||||
verify(mHalTunerSessionMock).stopProgramListUpdates();
|
||||
}
|
||||
|
||||
private void openAidlClients(int numClients) throws RemoteException {
|
||||
mAidlTunerCallbackMocks = new android.hardware.radio.ITunerCallback[numClients];
|
||||
mTunerSessions = new TunerSession[numClients];
|
||||
for (int i = 0; i < numClients; i++) {
|
||||
mAidlTunerCallbackMocks[i] = mock(android.hardware.radio.ITunerCallback.class);
|
||||
mTunerSessions[i] = mRadioModule.openSession(mAidlTunerCallbackMocks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateHalProgramInfo(boolean purge, List<RadioManager.ProgramInfo> modified,
|
||||
List<ProgramSelector.Identifier> removed) throws RemoteException {
|
||||
ProgramListChunk programListChunk = new ProgramListChunk();
|
||||
programListChunk.purge = purge;
|
||||
programListChunk.complete = true;
|
||||
if (modified != null) {
|
||||
for (RadioManager.ProgramInfo mod : modified) {
|
||||
programListChunk.modified.add(TestUtils.programInfoToHal(mod));
|
||||
}
|
||||
}
|
||||
if (removed != null) {
|
||||
for (ProgramSelector.Identifier id : removed) {
|
||||
programListChunk.removed.add(Convert.programIdentifierToHal(id));
|
||||
}
|
||||
}
|
||||
mHalTunerCallback.onProgramListUpdated(programListChunk);
|
||||
}
|
||||
|
||||
private void verifyAidlClientReceivedChunk(android.hardware.radio.ITunerCallback clientMock,
|
||||
boolean purge, List<RadioManager.ProgramInfo> modified,
|
||||
List<ProgramSelector.Identifier> removed) throws RemoteException {
|
||||
HashSet<RadioManager.ProgramInfo> modifiedSet = new HashSet<>();
|
||||
if (modified != null) {
|
||||
modifiedSet.addAll(modified);
|
||||
}
|
||||
HashSet<ProgramSelector.Identifier> removedSet = new HashSet<>();
|
||||
if (removed != null) {
|
||||
removedSet.addAll(removed);
|
||||
}
|
||||
ProgramList.Chunk expectedChunk = new ProgramList.Chunk(purge, true, modifiedSet,
|
||||
removedSet);
|
||||
verify(clientMock).onProgramListUpdated(argThat(new ChunkMatcher(expectedChunk)));
|
||||
}
|
||||
|
||||
// TODO(b/130750904): Remove this class and replace "argThat(new ChunkMatcher(chunk))" with
|
||||
// "eq(chunk)".
|
||||
//
|
||||
// Ideally, this class wouldn't exist, but currently RadioManager.ProgramInfo#hashCode() can
|
||||
// return different values for objects that satisfy ProgramInfo#equals(). As a short term
|
||||
// workaround, this class performs the O(N^2) comparison between the Chunks' mModified sets.
|
||||
//
|
||||
// To test if ProgramInfo#hashCode() has been fixed, remove commenting from
|
||||
// testProgramInfoHashCode() below.
|
||||
private class ChunkMatcher implements ArgumentMatcher<ProgramList.Chunk> {
|
||||
private final ProgramList.Chunk mExpected;
|
||||
|
||||
ChunkMatcher(ProgramList.Chunk expected) {
|
||||
mExpected = expected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(ProgramList.Chunk actual) {
|
||||
if ((mExpected.isPurge() != actual.isPurge())
|
||||
|| (mExpected.isComplete() != actual.isComplete())
|
||||
|| (!mExpected.getRemoved().equals(actual.getRemoved()))) {
|
||||
return false;
|
||||
}
|
||||
Set<RadioManager.ProgramInfo> expectedModified = mExpected.getModified();
|
||||
Set<RadioManager.ProgramInfo> actualModified = new HashSet<>(actual.getModified());
|
||||
if (expectedModified.size() != actualModified.size()) {
|
||||
return false;
|
||||
}
|
||||
for (RadioManager.ProgramInfo expectedInfo : expectedModified) {
|
||||
boolean found = false;
|
||||
for (RadioManager.ProgramInfo actualInfo : actualModified) {
|
||||
if (expectedInfo.equals(actualInfo)) {
|
||||
found = true;
|
||||
actualModified.remove(actualInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// @Test
|
||||
// public void testProgramInfoHashCode() {
|
||||
// RadioManager.ProgramInfo info1 = TestUtils.makeProgramInfo(
|
||||
// ProgramSelector.PROGRAM_TYPE_FM, mAmFmIdentifier, 0);
|
||||
// RadioManager.ProgramInfo info2 = TestUtils.makeProgramInfo(
|
||||
// ProgramSelector.PROGRAM_TYPE_FM, mAmFmIdentifier, 0);
|
||||
// assertEquals(info1, info2);
|
||||
// assertEquals(info1.hashCode(), info2.hashCode());
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.hardware.broadcastradio.V2_0.ProgramInfo;
|
||||
import android.hardware.radio.ProgramSelector;
|
||||
import android.hardware.radio.RadioManager;
|
||||
import android.hardware.radio.RadioMetadata;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
final class TestUtils {
|
||||
static RadioManager.ProgramInfo makeProgramInfo(int programType,
|
||||
ProgramSelector.Identifier identifier, int signalQuality) {
|
||||
// Note: If you set new fields, check if programInfoToHal() needs to be updated as well.
|
||||
return new RadioManager.ProgramInfo(new ProgramSelector(programType, identifier, null,
|
||||
null), null, null, null, 0, signalQuality, new RadioMetadata.Builder().build(),
|
||||
new HashMap<String, String>());
|
||||
}
|
||||
|
||||
static ProgramInfo programInfoToHal(RadioManager.ProgramInfo info) {
|
||||
// Note that because Convert does not by design provide functions for all conversions, this
|
||||
// function only copies fields that are set by makeProgramInfo().
|
||||
ProgramInfo hwInfo = new ProgramInfo();
|
||||
hwInfo.selector = Convert.programSelectorToHal(info.getSelector());
|
||||
hwInfo.signalQuality = info.getSignalStrength();
|
||||
return hwInfo;
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import android.hardware.broadcastradio.V2_0.MetadataKey;
|
||||
import android.hardware.broadcastradio.V2_0.ProgramFilter;
|
||||
import android.hardware.broadcastradio.V2_0.ProgramIdentifier;
|
||||
import android.hardware.broadcastradio.V2_0.ProgramInfo;
|
||||
import android.hardware.broadcastradio.V2_0.ProgramInfoFlags;
|
||||
import android.hardware.broadcastradio.V2_0.ProgramListChunk;
|
||||
import android.hardware.broadcastradio.V2_0.Properties;
|
||||
import android.hardware.broadcastradio.V2_0.Result;
|
||||
|
||||
@@ -48,6 +48,10 @@ class ProgramInfoCache {
|
||||
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;
|
||||
@@ -58,9 +62,10 @@ class ProgramInfoCache {
|
||||
|
||||
// Constructor for testing.
|
||||
@VisibleForTesting
|
||||
ProgramInfoCache(@Nullable ProgramList.Filter filter,
|
||||
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);
|
||||
}
|
||||
@@ -77,15 +82,23 @@ class ProgramInfoCache {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder("ProgramInfoCache(");
|
||||
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(")");
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return mComplete;
|
||||
}
|
||||
|
||||
public @Nullable ProgramList.Filter getFilter() {
|
||||
return mFilter;
|
||||
}
|
||||
@@ -102,6 +115,7 @@ class ProgramInfoCache {
|
||||
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,
|
||||
@@ -122,26 +136,18 @@ class ProgramInfoCache {
|
||||
purge = true;
|
||||
}
|
||||
|
||||
Set<Integer> idTypes = mFilter != null ? mFilter.getIdentifierTypes() : null;
|
||||
Set<ProgramSelector.Identifier> ids = mFilter != null ? mFilter.getIdentifiers() : null;
|
||||
boolean includeCategories = mFilter != null ? mFilter.areCategoriesIncluded() : true;
|
||||
boolean includeModifications = mFilter != null ? !mFilter.areModificationsExcluded() : 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 ((idTypes != null && !idTypes.isEmpty() && !idTypes.contains(id.getType()))
|
||||
|| (ids != null && !ids.isEmpty() && !ids.contains(id))
|
||||
|| (!includeCategories && id.isCategoryType())) {
|
||||
if (!passesFilter(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
removed.remove(id);
|
||||
RadioManager.ProgramInfo oldInfo = mProgramInfoMap.get(id);
|
||||
|
||||
RadioManager.ProgramInfo newInfo = entry.getValue();
|
||||
if (oldInfo != null && (!includeModifications || oldInfo.equals(newInfo))) {
|
||||
if (!shouldIncludeInModified(newInfo)) {
|
||||
continue;
|
||||
}
|
||||
mProgramInfoMap.put(id, newInfo);
|
||||
@@ -150,14 +156,81 @@ class ProgramInfoCache {
|
||||
for (ProgramSelector.Identifier rem : removed) {
|
||||
mProgramInfoMap.remove(rem);
|
||||
}
|
||||
return buildChunks(purge, modified, maxNumModifiedPerChunk, removed, maxNumRemovedPerChunk);
|
||||
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 @NonNull List<ProgramList.Chunk> buildChunks(boolean purge,
|
||||
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.
|
||||
@@ -205,8 +278,8 @@ class ProgramInfoCache {
|
||||
removedChunk.add(removedIter.next());
|
||||
}
|
||||
}
|
||||
chunks.add(new ProgramList.Chunk(purge && i == 0, i == numChunks - 1, modifiedChunk,
|
||||
removedChunk));
|
||||
chunks.add(new ProgramList.Chunk(purge && i == 0, complete && (i == numChunks - 1),
|
||||
modifiedChunk, removedChunk));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import android.util.MutableInt;
|
||||
import android.util.Slog;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
@@ -64,7 +65,13 @@ class RadioModule {
|
||||
private Boolean mAntennaConnected = null;
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private RadioManager.ProgramInfo mProgramInfo = null;
|
||||
private RadioManager.ProgramInfo mCurrentProgramInfo = null;
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private final ProgramInfoCache mProgramInfoCache = new ProgramInfoCache(null);
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private android.hardware.radio.ProgramList.Filter mUnionOfAidlProgramFilters = null;
|
||||
|
||||
// Callback registered with the HAL to relay callbacks to AIDL clients.
|
||||
private final ITunerCallback mHalTunerCallback = new ITunerCallback.Stub() {
|
||||
@@ -78,17 +85,22 @@ class RadioModule {
|
||||
public void onCurrentProgramInfoChanged(ProgramInfo halProgramInfo) {
|
||||
RadioManager.ProgramInfo programInfo = Convert.programInfoFromHal(halProgramInfo);
|
||||
synchronized (mLock) {
|
||||
mProgramInfo = programInfo;
|
||||
mCurrentProgramInfo = programInfo;
|
||||
fanoutAidlCallbackLocked(cb -> cb.onCurrentProgramInfoChanged(programInfo));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgramListUpdated(ProgramListChunk programListChunk) {
|
||||
// TODO: Cache per-AIDL client filters, send union of filters to HAL, use filters to fan
|
||||
// back out to clients.
|
||||
fanoutAidlCallback(cb -> cb.onProgramListUpdated(Convert.programListChunkFromHal(
|
||||
programListChunk)));
|
||||
synchronized (mLock) {
|
||||
android.hardware.radio.ProgramList.Chunk chunk =
|
||||
Convert.programListChunkFromHal(programListChunk);
|
||||
mProgramInfoCache.filterAndApplyChunk(chunk);
|
||||
|
||||
for (TunerSession tunerSession : mAidlTunerSessions) {
|
||||
tunerSession.onMergedProgramListUpdateFromHal(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -109,8 +121,9 @@ class RadioModule {
|
||||
@GuardedBy("mLock")
|
||||
private final Set<TunerSession> mAidlTunerSessions = new HashSet<>();
|
||||
|
||||
private RadioModule(@NonNull IBroadcastRadio service,
|
||||
@NonNull RadioManager.ModuleProperties properties) throws RemoteException {
|
||||
@VisibleForTesting
|
||||
RadioModule(@NonNull IBroadcastRadio service,
|
||||
@NonNull RadioManager.ModuleProperties properties) {
|
||||
mProperties = Objects.requireNonNull(properties);
|
||||
mService = Objects.requireNonNull(service);
|
||||
}
|
||||
@@ -163,8 +176,8 @@ class RadioModule {
|
||||
if (mAntennaConnected != null) {
|
||||
userCb.onAntennaState(mAntennaConnected);
|
||||
}
|
||||
if (mProgramInfo != null) {
|
||||
userCb.onCurrentProgramInfoChanged(mProgramInfo);
|
||||
if (mCurrentProgramInfo != null) {
|
||||
userCb.onCurrentProgramInfoChanged(mCurrentProgramInfo);
|
||||
}
|
||||
|
||||
return tunerSession;
|
||||
@@ -186,18 +199,114 @@ class RadioModule {
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable android.hardware.radio.ProgramList.Filter
|
||||
buildUnionOfTunerSessionFiltersLocked() {
|
||||
Set<Integer> idTypes = null;
|
||||
Set<android.hardware.radio.ProgramSelector.Identifier> ids = null;
|
||||
boolean includeCategories = false;
|
||||
boolean excludeModifications = true;
|
||||
|
||||
for (TunerSession tunerSession : mAidlTunerSessions) {
|
||||
android.hardware.radio.ProgramList.Filter filter =
|
||||
tunerSession.getProgramListFilter();
|
||||
if (filter == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (idTypes == null) {
|
||||
idTypes = new HashSet<>(filter.getIdentifierTypes());
|
||||
ids = new HashSet<>(filter.getIdentifiers());
|
||||
includeCategories = filter.areCategoriesIncluded();
|
||||
excludeModifications = filter.areModificationsExcluded();
|
||||
continue;
|
||||
}
|
||||
if (!idTypes.isEmpty()) {
|
||||
if (filter.getIdentifierTypes().isEmpty()) {
|
||||
idTypes.clear();
|
||||
} else {
|
||||
idTypes.addAll(filter.getIdentifierTypes());
|
||||
}
|
||||
}
|
||||
|
||||
if (!ids.isEmpty()) {
|
||||
if (filter.getIdentifiers().isEmpty()) {
|
||||
ids.clear();
|
||||
} else {
|
||||
ids.addAll(filter.getIdentifiers());
|
||||
}
|
||||
}
|
||||
|
||||
includeCategories |= filter.areCategoriesIncluded();
|
||||
excludeModifications &= filter.areModificationsExcluded();
|
||||
}
|
||||
|
||||
return idTypes == null ? null : new android.hardware.radio.ProgramList.Filter(idTypes, ids,
|
||||
includeCategories, excludeModifications);
|
||||
}
|
||||
|
||||
void onTunerSessionProgramListFilterChanged(@Nullable TunerSession session) {
|
||||
synchronized (mLock) {
|
||||
onTunerSessionProgramListFilterChangedLocked(session);
|
||||
}
|
||||
}
|
||||
|
||||
private void onTunerSessionProgramListFilterChangedLocked(@Nullable TunerSession session) {
|
||||
android.hardware.radio.ProgramList.Filter newFilter =
|
||||
buildUnionOfTunerSessionFiltersLocked();
|
||||
if (newFilter == null) {
|
||||
// If there are no AIDL clients remaining, we can stop updates from the HAL as well.
|
||||
if (mUnionOfAidlProgramFilters == null) {
|
||||
return;
|
||||
}
|
||||
mUnionOfAidlProgramFilters = null;
|
||||
try {
|
||||
mHalTunerSession.stopProgramListUpdates();
|
||||
} catch (RemoteException ex) {
|
||||
Slog.e(TAG, "mHalTunerSession.stopProgramListUpdates() failed: ", ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If the HAL filter doesn't change, we can immediately send an update to the AIDL
|
||||
// client.
|
||||
if (newFilter.equals(mUnionOfAidlProgramFilters)) {
|
||||
if (session != null) {
|
||||
session.updateProgramInfoFromHalCache(mProgramInfoCache);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, update the HAL's filter, and AIDL clients will be updated when
|
||||
// mHalTunerCallback.onProgramListUpdated() is called.
|
||||
mUnionOfAidlProgramFilters = newFilter;
|
||||
try {
|
||||
int halResult = mHalTunerSession.startProgramListUpdates(Convert.programFilterToHal(
|
||||
newFilter));
|
||||
Convert.throwOnError("startProgramListUpdates", halResult);
|
||||
} catch (RemoteException ex) {
|
||||
Slog.e(TAG, "mHalTunerSession.startProgramListUpdates() failed: ", ex);
|
||||
}
|
||||
}
|
||||
|
||||
void onTunerSessionClosed(TunerSession tunerSession) {
|
||||
synchronized (mLock) {
|
||||
onTunerSessionsClosedLocked(tunerSession);
|
||||
}
|
||||
}
|
||||
|
||||
private void onTunerSessionsClosedLocked(TunerSession... tunerSessions) {
|
||||
for (TunerSession tunerSession : tunerSessions) {
|
||||
mAidlTunerSessions.remove(tunerSession);
|
||||
if (mAidlTunerSessions.isEmpty() && mHalTunerSession != null) {
|
||||
Slog.v(TAG, "closing HAL tuner session");
|
||||
try {
|
||||
mHalTunerSession.close();
|
||||
} catch (RemoteException ex) {
|
||||
Slog.e(TAG, "mHalTunerSession.close() failed: ", ex);
|
||||
}
|
||||
mHalTunerSession = null;
|
||||
}
|
||||
onTunerSessionProgramListFilterChanged(null);
|
||||
if (mAidlTunerSessions.isEmpty() && mHalTunerSession != null) {
|
||||
Slog.v(TAG, "closing HAL tuner session");
|
||||
try {
|
||||
mHalTunerSession.close();
|
||||
} catch (RemoteException ex) {
|
||||
Slog.e(TAG, "mHalTunerSession.close() failed: ", ex);
|
||||
}
|
||||
mHalTunerSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,18 +322,25 @@ class RadioModule {
|
||||
}
|
||||
|
||||
private void fanoutAidlCallbackLocked(AidlCallbackRunnable runnable) {
|
||||
List<TunerSession> deadSessions = null;
|
||||
for (TunerSession tunerSession : mAidlTunerSessions) {
|
||||
try {
|
||||
runnable.run(tunerSession.mCallback);
|
||||
} catch (DeadObjectException ex) {
|
||||
// The other side died without calling close(), so just purge it from our
|
||||
// records.
|
||||
// The other side died without calling close(), so just purge it from our records.
|
||||
Slog.e(TAG, "Removing dead TunerSession");
|
||||
mAidlTunerSessions.remove(tunerSession);
|
||||
if (deadSessions == null) {
|
||||
deadSessions = new ArrayList<>();
|
||||
}
|
||||
deadSessions.add(tunerSession);
|
||||
} catch (RemoteException ex) {
|
||||
Slog.e(TAG, "Failed to invoke ITunerCallback: ", ex);
|
||||
}
|
||||
}
|
||||
if (deadSessions != null) {
|
||||
onTunerSessionsClosedLocked(deadSessions.toArray(
|
||||
new TunerSession[deadSessions.size()]));
|
||||
}
|
||||
}
|
||||
|
||||
public android.hardware.radio.ICloseHandle addAnnouncementListener(@NonNull int[] enabledTypes,
|
||||
|
||||
@@ -46,6 +46,7 @@ class TunerSession extends ITuner.Stub {
|
||||
final android.hardware.radio.ITunerCallback mCallback;
|
||||
private boolean mIsClosed = false;
|
||||
private boolean mIsMuted = false;
|
||||
private ProgramInfoCache mProgramInfoCache = null;
|
||||
|
||||
// necessary only for older APIs compatibility
|
||||
private RadioManager.BandConfig mDummyConfig = null;
|
||||
@@ -187,8 +188,51 @@ class TunerSession extends ITuner.Stub {
|
||||
public void startProgramListUpdates(ProgramList.Filter filter) throws RemoteException {
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
int halResult = mHwSession.startProgramListUpdates(Convert.programFilterToHal(filter));
|
||||
Convert.throwOnError("startProgramListUpdates", halResult);
|
||||
mProgramInfoCache = new ProgramInfoCache(filter);
|
||||
}
|
||||
// Note: RadioModule.onTunerSessionProgramListFilterChanged() must be called without mLock
|
||||
// held since it can call getProgramListFilter() and onHalProgramInfoUpdated().
|
||||
mModule.onTunerSessionProgramListFilterChanged(this);
|
||||
}
|
||||
|
||||
ProgramList.Filter getProgramListFilter() {
|
||||
synchronized (mLock) {
|
||||
return mProgramInfoCache == null ? null : mProgramInfoCache.getFilter();
|
||||
}
|
||||
}
|
||||
|
||||
void onMergedProgramListUpdateFromHal(ProgramList.Chunk mergedChunk) {
|
||||
List<ProgramList.Chunk> clientUpdateChunks = null;
|
||||
synchronized (mLock) {
|
||||
if (mProgramInfoCache == null) {
|
||||
return;
|
||||
}
|
||||
clientUpdateChunks = mProgramInfoCache.filterAndApplyChunk(mergedChunk);
|
||||
}
|
||||
dispatchClientUpdateChunks(clientUpdateChunks);
|
||||
}
|
||||
|
||||
void updateProgramInfoFromHalCache(ProgramInfoCache halCache) {
|
||||
List<ProgramList.Chunk> clientUpdateChunks = null;
|
||||
synchronized (mLock) {
|
||||
if (mProgramInfoCache == null) {
|
||||
return;
|
||||
}
|
||||
clientUpdateChunks = mProgramInfoCache.filterAndUpdateFrom(halCache, true);
|
||||
}
|
||||
dispatchClientUpdateChunks(clientUpdateChunks);
|
||||
}
|
||||
|
||||
private void dispatchClientUpdateChunks(@Nullable List<ProgramList.Chunk> chunks) {
|
||||
if (chunks == null) {
|
||||
return;
|
||||
}
|
||||
for (ProgramList.Chunk chunk : chunks) {
|
||||
try {
|
||||
mCallback.onProgramListUpdated(chunk);
|
||||
} catch (RemoteException ex) {
|
||||
Slog.w(TAG, "mCallback.onProgramListUpdated() failed: ", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,8 +240,11 @@ class TunerSession extends ITuner.Stub {
|
||||
public void stopProgramListUpdates() throws RemoteException {
|
||||
synchronized (mLock) {
|
||||
checkNotClosedLocked();
|
||||
mHwSession.stopProgramListUpdates();
|
||||
mProgramInfoCache = null;
|
||||
}
|
||||
// Note: RadioModule.onTunerSessionProgramListFilterChanged() must be called without mLock
|
||||
// held since it can call getProgramListFilter() and onHalProgramInfoUpdated().
|
||||
mModule.onTunerSessionProgramListFilterChanged(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user