1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.server.broadcastradio.hal2;
17 
18 import static org.junit.Assert.*;
19 import static org.mockito.Matchers.any;
20 import static org.mockito.Mockito.doAnswer;
21 import static org.mockito.Mockito.mock;
22 import static org.mockito.Mockito.timeout;
23 import static org.mockito.Mockito.times;
24 import static org.mockito.Mockito.verify;
25 import static org.mockito.Mockito.when;
26 
27 import android.hardware.broadcastradio.V2_0.IBroadcastRadio;
28 import android.hardware.broadcastradio.V2_0.ITunerCallback;
29 import android.hardware.broadcastradio.V2_0.ITunerSession;
30 import android.hardware.broadcastradio.V2_0.ProgramFilter;
31 import android.hardware.broadcastradio.V2_0.ProgramListChunk;
32 import android.hardware.broadcastradio.V2_0.Result;
33 import android.hardware.radio.ProgramList;
34 import android.hardware.radio.ProgramSelector;
35 import android.hardware.radio.RadioManager;
36 import android.os.RemoteException;
37 import android.test.suitebuilder.annotation.MediumTest;
38 
39 import androidx.test.runner.AndroidJUnit4;
40 
41 import org.junit.Before;
42 import org.junit.Test;
43 import org.junit.runner.RunWith;
44 import org.mockito.Mock;
45 import org.mockito.MockitoAnnotations;
46 import org.mockito.stubbing.Answer;
47 import org.mockito.verification.VerificationWithTimeout;
48 
49 import java.util.Arrays;
50 import java.util.HashSet;
51 import java.util.List;
52 
53 /**
54  * Tests for v2 HAL RadioModule.
55  */
56 @RunWith(AndroidJUnit4.class)
57 @MediumTest
58 public class StartProgramListUpdatesFanoutTest {
59     private static final String TAG = "BroadcastRadioTests.hal2.StartProgramListUpdatesFanout";
60 
61     private static final VerificationWithTimeout CB_TIMEOUT = timeout(100);
62 
63     // Mocks
64     @Mock IBroadcastRadio mBroadcastRadioMock;
65     @Mock ITunerSession mHalTunerSessionMock;
66     private android.hardware.radio.ITunerCallback[] mAidlTunerCallbackMocks;
67 
68     private final Object mLock = new Object();
69     // RadioModule under test
70     private RadioModule mRadioModule;
71 
72     // Objects created by mRadioModule
73     private ITunerCallback mHalTunerCallback;
74     private TunerSession[] mTunerSessions;
75 
76     // Data objects used during tests
77     private final ProgramSelector.Identifier mAmFmIdentifier =
78             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, 88500);
79     private final RadioManager.ProgramInfo mAmFmInfo = TestUtils.makeProgramInfo(
80             ProgramSelector.PROGRAM_TYPE_FM, mAmFmIdentifier, 0);
81     private final RadioManager.ProgramInfo mModifiedAmFmInfo = TestUtils.makeProgramInfo(
82             ProgramSelector.PROGRAM_TYPE_FM, mAmFmIdentifier, 1);
83 
84     private final ProgramSelector.Identifier mRdsIdentifier =
85             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_RDS_PI, 15019);
86     private final RadioManager.ProgramInfo mRdsInfo = TestUtils.makeProgramInfo(
87             ProgramSelector.PROGRAM_TYPE_FM, mRdsIdentifier, 0);
88 
89     private final ProgramSelector.Identifier mDabEnsembleIdentifier =
90             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, 1337);
91     private final RadioManager.ProgramInfo mDabEnsembleInfo = TestUtils.makeProgramInfo(
92             ProgramSelector.PROGRAM_TYPE_DAB, mDabEnsembleIdentifier, 0);
93 
94     @Before
setup()95     public void setup() throws RemoteException {
96         MockitoAnnotations.initMocks(this);
97 
98         mRadioModule = new RadioModule(mBroadcastRadioMock, new RadioManager.ModuleProperties(0, "",
99                   0, "", "", "", "", 0, 0, false, false, null, false, new int[] {}, new int[] {},
100                   null, null), mLock);
101 
102         doAnswer((Answer) invocation -> {
103             mHalTunerCallback = (ITunerCallback) invocation.getArguments()[0];
104             IBroadcastRadio.openSessionCallback cb = (IBroadcastRadio.openSessionCallback)
105                     invocation.getArguments()[1];
106             cb.onValues(Result.OK, mHalTunerSessionMock);
107             return null;
108         }).when(mBroadcastRadioMock).openSession(any(), any());
109         when(mHalTunerSessionMock.startProgramListUpdates(any())).thenReturn(Result.OK);
110     }
111 
112     @Test
testFanout()113     public void testFanout() throws RemoteException {
114         // Open 3 clients that will all use the same filter, and start updates on two of them for
115         // now. The HAL TunerSession should only see 1 filter update.
116         openAidlClients(3);
117         ProgramList.Filter aidlFilter = new ProgramList.Filter(new HashSet<Integer>(),
118                 new HashSet<ProgramSelector.Identifier>(), true, false);
119         ProgramFilter halFilter = Convert.programFilterToHal(aidlFilter);
120         for (int i = 0; i < 2; i++) {
121             mTunerSessions[i].startProgramListUpdates(aidlFilter);
122         }
123         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
124 
125         // Initiate a program list update from the HAL side and verify both connected AIDL clients
126         // receive the update.
127         updateHalProgramInfo(true, Arrays.asList(mAmFmInfo, mRdsInfo), null);
128         for (int i = 0; i < 2; i++) {
129             verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[i], true, Arrays.asList(
130                     mAmFmInfo, mRdsInfo), null);
131         }
132 
133         // Repeat with a non-purging update.
134         updateHalProgramInfo(false, Arrays.asList(mModifiedAmFmInfo),
135                 Arrays.asList(mRdsIdentifier));
136         for (int i = 0; i < 2; i++) {
137             verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[i], false,
138                     Arrays.asList(mModifiedAmFmInfo), Arrays.asList(mRdsIdentifier));
139         }
140 
141         // Now start updates on the 3rd client. Verify the HAL function has not been called again
142         // and client receives the appropriate update.
143         mTunerSessions[2].startProgramListUpdates(aidlFilter);
144         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(any());
145         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[2], true,
146                 Arrays.asList(mModifiedAmFmInfo), null);
147     }
148 
149     @Test
testFiltering()150     public void testFiltering() throws RemoteException {
151         // Open 4 clients that will use the following filters:
152         // [0]: ID mRdsIdentifier, modifications excluded
153         // [1]: No categories, modifications excluded
154         // [2]: Type IDENTIFIER_TYPE_AMFM_FREQUENCY, modifications excluded
155         // [3]: Type IDENTIFIER_TYPE_AMFM_FREQUENCY, modifications included
156         openAidlClients(4);
157         ProgramList.Filter idFilter = new ProgramList.Filter(new HashSet<Integer>(),
158                 new HashSet<ProgramSelector.Identifier>(Arrays.asList(mRdsIdentifier)), true, true);
159         ProgramList.Filter categoryFilter = new ProgramList.Filter(new HashSet<Integer>(),
160                 new HashSet<ProgramSelector.Identifier>(), false, true);
161         ProgramList.Filter typeFilterWithoutModifications = new ProgramList.Filter(
162                 new HashSet<Integer>(Arrays.asList(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)),
163                 new HashSet<ProgramSelector.Identifier>(), true, true);
164         ProgramList.Filter typeFilterWithModifications = new ProgramList.Filter(
165                 new HashSet<Integer>(Arrays.asList(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)),
166                 new HashSet<ProgramSelector.Identifier>(), true, false);
167 
168         // Start updates on the clients in order. The HAL filter should get updated after each
169         // client except [2].
170         mTunerSessions[0].startProgramListUpdates(idFilter);
171         ProgramFilter halFilter = Convert.programFilterToHal(idFilter);
172         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
173 
174         mTunerSessions[1].startProgramListUpdates(categoryFilter);
175         halFilter.identifiers.clear();
176         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
177 
178         mTunerSessions[2].startProgramListUpdates(typeFilterWithoutModifications);
179         verify(mHalTunerSessionMock, times(2)).startProgramListUpdates(any());
180 
181         mTunerSessions[3].startProgramListUpdates(typeFilterWithModifications);
182         halFilter.excludeModifications = false;
183         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
184 
185         // Adding mRdsInfo should update clients [0] and [1].
186         updateHalProgramInfo(false, Arrays.asList(mRdsInfo), null);
187         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], false, Arrays.asList(mRdsInfo),
188                 null);
189         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], false, Arrays.asList(mRdsInfo),
190                 null);
191 
192         // Adding mAmFmInfo should update clients [1], [2], and [3].
193         updateHalProgramInfo(false, Arrays.asList(mAmFmInfo), null);
194         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], false, Arrays.asList(mAmFmInfo),
195                 null);
196         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[2], false, Arrays.asList(mAmFmInfo),
197                 null);
198         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[3], false, Arrays.asList(mAmFmInfo),
199                 null);
200 
201         // Modifying mAmFmInfo to mModifiedAmFmInfo should update only [3].
202         updateHalProgramInfo(false, Arrays.asList(mModifiedAmFmInfo), null);
203         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[3], false,
204                 Arrays.asList(mModifiedAmFmInfo), null);
205 
206         // Adding mDabEnsembleInfo should not update any client.
207         updateHalProgramInfo(false, Arrays.asList(mDabEnsembleInfo), null);
208         verify(mAidlTunerCallbackMocks[0], CB_TIMEOUT.times(1)).onProgramListUpdated(any());
209         verify(mAidlTunerCallbackMocks[1], CB_TIMEOUT.times(2)).onProgramListUpdated(any());
210         verify(mAidlTunerCallbackMocks[2], CB_TIMEOUT.times(1)).onProgramListUpdated(any());
211         verify(mAidlTunerCallbackMocks[3], CB_TIMEOUT.times(2)).onProgramListUpdated(any());
212     }
213 
214     @Test
testClientClosing()215     public void testClientClosing() throws RemoteException {
216         // Open 2 clients that use different filters that are both sensitive to mAmFmIdentifier.
217         openAidlClients(2);
218         ProgramList.Filter idFilter = new ProgramList.Filter(new HashSet<Integer>(),
219                 new HashSet<ProgramSelector.Identifier>(Arrays.asList(mAmFmIdentifier)), true,
220                 false);
221         ProgramList.Filter typeFilter = new ProgramList.Filter(
222                 new HashSet<Integer>(Arrays.asList(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)),
223                 new HashSet<ProgramSelector.Identifier>(), true, false);
224 
225         // Start updates on the clients, and verify the HAL filter is updated after each one.
226         mTunerSessions[0].startProgramListUpdates(idFilter);
227         ProgramFilter halFilter = Convert.programFilterToHal(idFilter);
228         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
229 
230         mTunerSessions[1].startProgramListUpdates(typeFilter);
231         halFilter.identifiers.clear();
232         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
233 
234         // Update the HAL with mAmFmInfo, and verify both clients are updated.
235         updateHalProgramInfo(true, Arrays.asList(mAmFmInfo), null);
236         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], true, Arrays.asList(mAmFmInfo),
237                 null);
238         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], true, Arrays.asList(mAmFmInfo),
239                 null);
240 
241         // Stop updates on the first client and verify the HAL filter is updated.
242         mTunerSessions[0].stopProgramListUpdates();
243         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(Convert.programFilterToHal(
244                 typeFilter));
245 
246         // Update the HAL with mModifiedAmFmInfo, and verify only the remaining client is updated.
247         updateHalProgramInfo(true, Arrays.asList(mModifiedAmFmInfo), null);
248         verify(mAidlTunerCallbackMocks[0], CB_TIMEOUT.times(1)).onProgramListUpdated(any());
249         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], true,
250                 Arrays.asList(mModifiedAmFmInfo), null);
251 
252         // Close the other client without explicitly stopping updates, and verify HAL updates are
253         // stopped as well.
254         mTunerSessions[1].close();
255         verify(mHalTunerSessionMock).stopProgramListUpdates();
256     }
257 
258     @Test
testNullAidlFilter()259     public void testNullAidlFilter() throws RemoteException {
260         openAidlClients(1);
261         mTunerSessions[0].startProgramListUpdates(null);
262         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(any());
263 
264         // Verify the AIDL client receives all types of updates (e.g. a new program, an update to
265         // that program, and a category).
266         updateHalProgramInfo(true, Arrays.asList(mAmFmInfo, mRdsInfo), null);
267         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], true, Arrays.asList(
268                 mAmFmInfo, mRdsInfo), null);
269         updateHalProgramInfo(false, Arrays.asList(mModifiedAmFmInfo), null);
270         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], false,
271                 Arrays.asList(mModifiedAmFmInfo), null);
272         updateHalProgramInfo(false, Arrays.asList(mDabEnsembleInfo), null);
273         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], false,
274                 Arrays.asList(mDabEnsembleInfo), null);
275 
276         // Verify closing the AIDL session also stops HAL updates.
277         mTunerSessions[0].close();
278         verify(mHalTunerSessionMock).stopProgramListUpdates();
279     }
280 
openAidlClients(int numClients)281     private void openAidlClients(int numClients) throws RemoteException {
282         mAidlTunerCallbackMocks = new android.hardware.radio.ITunerCallback[numClients];
283         mTunerSessions = new TunerSession[numClients];
284         for (int i = 0; i < numClients; i++) {
285             mAidlTunerCallbackMocks[i] = mock(android.hardware.radio.ITunerCallback.class);
286             mTunerSessions[i] = mRadioModule.openSession(mAidlTunerCallbackMocks[i]);
287         }
288     }
289 
updateHalProgramInfo(boolean purge, List<RadioManager.ProgramInfo> modified, List<ProgramSelector.Identifier> removed)290     private void updateHalProgramInfo(boolean purge, List<RadioManager.ProgramInfo> modified,
291             List<ProgramSelector.Identifier> removed) throws RemoteException {
292         ProgramListChunk programListChunk = new ProgramListChunk();
293         programListChunk.purge = purge;
294         programListChunk.complete = true;
295         if (modified != null) {
296             for (RadioManager.ProgramInfo mod : modified) {
297                 programListChunk.modified.add(TestUtils.programInfoToHal(mod));
298             }
299         }
300         if (removed != null) {
301             for (ProgramSelector.Identifier id : removed) {
302                 programListChunk.removed.add(Convert.programIdentifierToHal(id));
303             }
304         }
305         mHalTunerCallback.onProgramListUpdated(programListChunk);
306     }
307 
verifyAidlClientReceivedChunk(android.hardware.radio.ITunerCallback clientMock, boolean purge, List<RadioManager.ProgramInfo> modified, List<ProgramSelector.Identifier> removed)308     private void verifyAidlClientReceivedChunk(android.hardware.radio.ITunerCallback clientMock,
309             boolean purge, List<RadioManager.ProgramInfo> modified,
310             List<ProgramSelector.Identifier> removed) throws RemoteException {
311         HashSet<RadioManager.ProgramInfo> modifiedSet = new HashSet<>();
312         if (modified != null) {
313             modifiedSet.addAll(modified);
314         }
315         HashSet<ProgramSelector.Identifier> removedSet = new HashSet<>();
316         if (removed != null) {
317             removedSet.addAll(removed);
318         }
319         ProgramList.Chunk expectedChunk = new ProgramList.Chunk(purge, true, modifiedSet,
320                 removedSet);
321         verify(clientMock, CB_TIMEOUT).onProgramListUpdated(expectedChunk);
322     }
323 }
324