summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/hardware/radio/ProgramList.java46
-rw-r--r--core/java/android/hardware/radio/RadioMetadata.java5
-rw-r--r--core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ProgramInfoCacheTest.java98
-rw-r--r--core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java353
-rw-r--r--core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java42
-rw-r--r--services/core/java/com/android/server/broadcastradio/hal2/Convert.java1
-rw-r--r--services/core/java/com/android/server/broadcastradio/hal2/ProgramInfoCache.java109
-rw-r--r--services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java158
-rw-r--r--services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java53
9 files changed, 768 insertions, 97 deletions
diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java
index 623d5ec434fe..f4fd1b6fb75f 100644
--- a/core/java/android/hardware/radio/ProgramList.java
+++ b/core/java/android/hardware/radio/ProgramList.java
@@ -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 + "]";
+ }
}
}
diff --git a/core/java/android/hardware/radio/RadioMetadata.java b/core/java/android/hardware/radio/RadioMetadata.java
index b60c13631edd..c135c8a73abc 100644
--- a/core/java/android/hardware/radio/RadioMetadata.java
+++ b/core/java/android/hardware/radio/RadioMetadata.java
@@ -258,6 +258,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;
if (!(obj instanceof RadioMetadata)) return false;
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ProgramInfoCacheTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ProgramInfoCacheTest.java
index b2254c5fe59e..eadf226b01ce 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ProgramInfoCacheTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ProgramInfoCacheTest.java
@@ -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());
}
}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java
new file mode 100644
index 000000000000..f9e37981fa6f
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java
@@ -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());
+ // }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java
new file mode 100644
index 000000000000..4944803eaafe
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java
@@ -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;
+ }
+}
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
index 9730c9a1a380..1a1845ac3d5f 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
@@ -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;
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/ProgramInfoCache.java b/services/core/java/com/android/server/broadcastradio/hal2/ProgramInfoCache.java
index b1bd39566ba6..8c9389101141 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/ProgramInfoCache.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/ProgramInfoCache.java
@@ -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;
}
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
index acb0207ff11f..53890a48a674 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
@@ -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,
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
index 008fea5831ad..764fca9a66b2 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
@@ -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