Merge "Rewire ProgramInfo updates for 1 HAL : N AIDL fanout"

This commit is contained in:
George Lu
2019-07-25 19:56:30 +00:00
committed by Android (Google) Code Review
9 changed files with 768 additions and 97 deletions

View File

@@ -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 + "]";
}
}
}

View File

@@ -257,6 +257,11 @@ public final class RadioMetadata implements Parcelable {
private final Bundle mBundle;
@Override
public int hashCode() {
return mBundle.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;

View File

@@ -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());
}
}

View File

@@ -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());
// }
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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