diff options
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 |