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 com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
19 
20 import static org.junit.Assert.*;
21 import static org.mockito.Matchers.any;
22 import static org.mockito.Mockito.doAnswer;
23 import static org.mockito.Mockito.mock;
24 import static org.mockito.Mockito.timeout;
25 import static org.mockito.Mockito.times;
26 import static org.mockito.Mockito.verify;
27 import static org.mockito.Mockito.when;
28 
29 import android.hardware.broadcastradio.V2_0.IBroadcastRadio;
30 import android.hardware.broadcastradio.V2_0.ITunerCallback;
31 import android.hardware.broadcastradio.V2_0.ITunerSession;
32 import android.hardware.broadcastradio.V2_0.ProgramFilter;
33 import android.hardware.broadcastradio.V2_0.ProgramListChunk;
34 import android.hardware.broadcastradio.V2_0.Result;
35 import android.hardware.radio.ProgramList;
36 import android.hardware.radio.ProgramSelector;
37 import android.hardware.radio.RadioManager;
38 import android.hardware.radio.UniqueProgramIdentifier;
39 import android.os.RemoteException;
40 
41 import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
42 import com.android.server.broadcastradio.ExtendedRadioMockitoTestCase;
43 import com.android.server.broadcastradio.RadioServiceUserController;
44 
45 import org.junit.Before;
46 import org.junit.Test;
47 import org.mockito.Mock;
48 import org.mockito.stubbing.Answer;
49 import org.mockito.verification.VerificationWithTimeout;
50 
51 import java.util.Arrays;
52 import java.util.HashSet;
53 import java.util.List;
54 
55 /**
56  * Tests for v2 HAL RadioModule.
57  */
58 public class StartProgramListUpdatesFanoutTest extends ExtendedRadioMockitoTestCase {
59     private static final String TAG = "BroadcastRadioTests.hal2.StartProgramListUpdatesFanout";
60 
61     private static final VerificationWithTimeout CB_TIMEOUT = timeout(500);
62 
63     // Mocks
64     @Mock IBroadcastRadio mBroadcastRadioMock;
65     @Mock ITunerSession mHalTunerSessionMock;
66     private android.hardware.radio.ITunerCallback[] mAidlTunerCallbackMocks;
67 
68     // RadioModule under test
69     private RadioModule mRadioModule;
70 
71     // Objects created by mRadioModule
72     private ITunerCallback mHalTunerCallback;
73     private TunerSession[] mTunerSessions;
74 
75     // Data objects used during tests
76     private static final int TEST_QUALITY = 0;
77     private static final ProgramSelector.Identifier TEST_AM_FM_ID =
78             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
79                     /* value= */ 88_500);
80     private static final ProgramSelector TEST_AM_FM_SELECTOR = TestUtils.makeProgramSelector(
81             ProgramSelector.PROGRAM_TYPE_FM, TEST_AM_FM_ID);
82     private static final RadioManager.ProgramInfo TEST_AM_FM_INFO = TestUtils.makeProgramInfo(
83             TEST_AM_FM_SELECTOR, TEST_QUALITY);
84     private static final RadioManager.ProgramInfo TEST_AM_FM_MODIFIED_INFO =
85             TestUtils.makeProgramInfo(TEST_AM_FM_SELECTOR, TEST_QUALITY + 1);
86 
87     private static final ProgramSelector.Identifier TEST_RDS_ID =
88             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_RDS_PI,
89                     /* value= */ 15_019);
90     private static final ProgramSelector TEST_RDS_SELECTOR = TestUtils.makeProgramSelector(
91             ProgramSelector.PROGRAM_TYPE_FM, TEST_RDS_ID);
92 
93     private static final UniqueProgramIdentifier TEST_RDS_UNIQUE_ID = new UniqueProgramIdentifier(
94             TEST_RDS_ID);
95     private static final RadioManager.ProgramInfo TEST_RDS_INFO = TestUtils.makeProgramInfo(
96             TEST_RDS_SELECTOR, TEST_QUALITY);
97 
98     private static final ProgramSelector.Identifier TEST_DAB_SID_ID =
99             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
100                     /* value= */ 0xA000000111L);
101     private static final ProgramSelector.Identifier TEST_DAB_ENSEMBLE_ID =
102             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
103                     /* value= */ 0x1001);
104     private static final ProgramSelector.Identifier TEST_DAB_FREQUENCY_ID =
105             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
106                     /* value= */ 220_352);
107     private static final ProgramSelector TEST_DAB_SELECTOR = TestUtils.makeProgramSelector(
108             ProgramSelector.PROGRAM_TYPE_DAB, TEST_DAB_SID_ID,
109             new ProgramSelector.Identifier[]{TEST_DAB_FREQUENCY_ID, TEST_DAB_ENSEMBLE_ID});
110     private static final RadioManager.ProgramInfo TEST_DAB_INFO = TestUtils.makeProgramInfo(
111             TEST_DAB_SELECTOR, TEST_QUALITY);
112 
113     @Override
initializeSession(StaticMockitoSessionBuilder builder)114     protected void initializeSession(StaticMockitoSessionBuilder builder) {
115         builder.spyStatic(RadioServiceUserController.class);
116     }
117 
118     @Before
setup()119     public void setup() throws RemoteException {
120         doReturn(true).when(() -> RadioServiceUserController.isCurrentOrSystemUser());
121 
122         mRadioModule = new RadioModule(mBroadcastRadioMock,
123                 TestUtils.makeDefaultModuleProperties());
124 
125         doAnswer((Answer) invocation -> {
126             mHalTunerCallback = (ITunerCallback) invocation.getArguments()[0];
127             IBroadcastRadio.openSessionCallback cb = (IBroadcastRadio.openSessionCallback)
128                     invocation.getArguments()[1];
129             cb.onValues(Result.OK, mHalTunerSessionMock);
130             return null;
131         }).when(mBroadcastRadioMock).openSession(any(), any());
132         when(mHalTunerSessionMock.startProgramListUpdates(any())).thenReturn(Result.OK);
133     }
134 
135     @Test
testFanout()136     public void testFanout() throws RemoteException {
137         // Open 3 clients that will all use the same filter, and start updates on two of them for
138         // now. The HAL TunerSession should only see 1 filter update.
139         openAidlClients(3);
140         ProgramList.Filter aidlFilter = new ProgramList.Filter(new HashSet<Integer>(),
141                 new HashSet<ProgramSelector.Identifier>(), true, false);
142         ProgramFilter halFilter = Convert.programFilterToHal(aidlFilter);
143         for (int i = 0; i < 2; i++) {
144             mTunerSessions[i].startProgramListUpdates(aidlFilter);
145         }
146         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
147 
148         // Initiate a program list update from the HAL side and verify both connected AIDL clients
149         // receive the update.
150         updateHalProgramInfo(true, Arrays.asList(TEST_AM_FM_INFO, TEST_RDS_INFO), null);
151         for (int i = 0; i < 2; i++) {
152             verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[i], true, Arrays.asList(
153                     TEST_AM_FM_INFO, TEST_RDS_INFO), null);
154         }
155 
156         // Repeat with a non-purging update.
157         updateHalProgramInfo(false, Arrays.asList(TEST_AM_FM_MODIFIED_INFO),
158                 Arrays.asList(TEST_RDS_ID));
159         for (int i = 0; i < 2; i++) {
160             verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[i], false,
161                     Arrays.asList(TEST_AM_FM_MODIFIED_INFO), Arrays.asList(TEST_RDS_UNIQUE_ID));
162         }
163 
164         // Now start updates on the 3rd client. Verify the HAL function has not been called again
165         // and client receives the appropriate update.
166         mTunerSessions[2].startProgramListUpdates(aidlFilter);
167         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(any());
168         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[2], true,
169                 Arrays.asList(TEST_AM_FM_MODIFIED_INFO), null);
170     }
171 
172     @Test
testFiltering()173     public void testFiltering() throws RemoteException {
174         // Open 4 clients that will use the following filters:
175         // [0]: ID TEST_RDS_ID, modifications excluded
176         // [1]: No categories, modifications excluded
177         // [2]: Type IDENTIFIER_TYPE_AMFM_FREQUENCY, modifications excluded
178         // [3]: Type IDENTIFIER_TYPE_AMFM_FREQUENCY, modifications included
179         openAidlClients(4);
180         ProgramList.Filter idFilter = new ProgramList.Filter(new HashSet<Integer>(),
181                 new HashSet<ProgramSelector.Identifier>(Arrays.asList(TEST_RDS_ID)), true, true);
182         ProgramList.Filter categoryFilter = new ProgramList.Filter(new HashSet<Integer>(),
183                 new HashSet<ProgramSelector.Identifier>(), false, true);
184         ProgramList.Filter typeFilterWithoutModifications = new ProgramList.Filter(
185                 new HashSet<Integer>(Arrays.asList(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)),
186                 new HashSet<ProgramSelector.Identifier>(), true, true);
187         ProgramList.Filter typeFilterWithModifications = new ProgramList.Filter(
188                 new HashSet<Integer>(Arrays.asList(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)),
189                 new HashSet<ProgramSelector.Identifier>(), true, false);
190 
191         // Start updates on the clients in order. The HAL filter should get updated after each
192         // client except [2]. Client [2] should update received chunk with an empty program
193         // list
194         mTunerSessions[0].startProgramListUpdates(idFilter);
195         ProgramFilter halFilter = Convert.programFilterToHal(idFilter);
196         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
197 
198         mTunerSessions[1].startProgramListUpdates(categoryFilter);
199         halFilter.identifiers.clear();
200         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
201 
202         mTunerSessions[2].startProgramListUpdates(typeFilterWithoutModifications);
203         verify(mHalTunerSessionMock, times(2)).startProgramListUpdates(any());
204         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[2], true, Arrays.asList(),
205                 null);
206         verify(mAidlTunerCallbackMocks[2], CB_TIMEOUT.times(1)).onProgramListUpdated(any());
207 
208         mTunerSessions[3].startProgramListUpdates(typeFilterWithModifications);
209         halFilter.excludeModifications = false;
210         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
211 
212         // Adding TEST_RDS_INFO should update clients [0] and [1].
213         updateHalProgramInfo(false, Arrays.asList(TEST_RDS_INFO), null);
214         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], false,
215                 Arrays.asList(TEST_RDS_INFO), null);
216         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], false,
217                 Arrays.asList(TEST_RDS_INFO), null);
218 
219         // Adding TEST_AM_FM_INFO should update clients [1], [2], and [3].
220         updateHalProgramInfo(false, Arrays.asList(TEST_AM_FM_INFO), null);
221         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], false,
222                 Arrays.asList(TEST_AM_FM_INFO), null);
223         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[2], false,
224                 Arrays.asList(TEST_AM_FM_INFO), null);
225         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[3], false,
226                 Arrays.asList(TEST_AM_FM_INFO), null);
227 
228         // Modifying TEST_AM_FM_INFO to TEST_AM_FM_MODIFIED_INFO should update only [3].
229         updateHalProgramInfo(false, Arrays.asList(TEST_AM_FM_MODIFIED_INFO), null);
230         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[3], false,
231                 Arrays.asList(TEST_AM_FM_MODIFIED_INFO), null);
232 
233         updateHalProgramInfo(false, Arrays.asList(TEST_DAB_INFO), null);
234         verify(mAidlTunerCallbackMocks[0], CB_TIMEOUT.times(1)).onProgramListUpdated(any());
235         verify(mAidlTunerCallbackMocks[1], CB_TIMEOUT.times(3)).onProgramListUpdated(any());
236         verify(mAidlTunerCallbackMocks[2], CB_TIMEOUT.times(2)).onProgramListUpdated(any());
237         verify(mAidlTunerCallbackMocks[3], CB_TIMEOUT.times(2)).onProgramListUpdated(any());
238     }
239 
240     @Test
testClientClosing()241     public void testClientClosing() throws RemoteException {
242         // Open 2 clients that use different filters that are both sensitive to TEST_AM_FM_ID.
243         openAidlClients(2);
244         ProgramList.Filter idFilter = new ProgramList.Filter(new HashSet<Integer>(),
245                 new HashSet<ProgramSelector.Identifier>(Arrays.asList(TEST_AM_FM_ID)), true,
246                 false);
247         ProgramList.Filter typeFilter = new ProgramList.Filter(
248                 new HashSet<Integer>(Arrays.asList(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)),
249                 new HashSet<ProgramSelector.Identifier>(), true, false);
250 
251         // Start updates on the clients, and verify the HAL filter is updated after each one.
252         mTunerSessions[0].startProgramListUpdates(idFilter);
253         ProgramFilter halFilter = Convert.programFilterToHal(idFilter);
254         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
255 
256         mTunerSessions[1].startProgramListUpdates(typeFilter);
257         halFilter.identifiers.clear();
258         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
259 
260         // Update the HAL with TEST_AM_FM_INFO, and verify both clients are updated.
261         updateHalProgramInfo(true, Arrays.asList(TEST_AM_FM_INFO), null);
262         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], true,
263                 Arrays.asList(TEST_AM_FM_INFO), null);
264         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], true,
265                 Arrays.asList(TEST_AM_FM_INFO), null);
266 
267         // Stop updates on the first client and verify the HAL filter is updated.
268         mTunerSessions[0].stopProgramListUpdates();
269         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(Convert.programFilterToHal(
270                 typeFilter));
271 
272         // Update the HAL with TEST_AM_FM_MODIFIED_INFO, and verify only the remaining client is
273         // updated.
274         updateHalProgramInfo(true, Arrays.asList(TEST_AM_FM_MODIFIED_INFO), null);
275         verify(mAidlTunerCallbackMocks[0], CB_TIMEOUT.times(1)).onProgramListUpdated(any());
276         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], true,
277                 Arrays.asList(TEST_AM_FM_MODIFIED_INFO), null);
278 
279         // Close the other client without explicitly stopping updates, and verify HAL updates are
280         // stopped as well.
281         mTunerSessions[1].close();
282         verify(mHalTunerSessionMock).stopProgramListUpdates();
283     }
284 
285     @Test
testNullAidlFilter()286     public void testNullAidlFilter() throws RemoteException {
287         openAidlClients(1);
288         mTunerSessions[0].startProgramListUpdates(null);
289         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(any());
290 
291         // Verify the AIDL client receives all types of updates (e.g. a new program, an update to
292         // that program, and a category).
293         updateHalProgramInfo(true, Arrays.asList(TEST_AM_FM_INFO, TEST_RDS_INFO), null);
294         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], true, Arrays.asList(
295                 TEST_AM_FM_INFO, TEST_RDS_INFO), null);
296         updateHalProgramInfo(false, Arrays.asList(TEST_AM_FM_MODIFIED_INFO), null);
297         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], false,
298                 Arrays.asList(TEST_AM_FM_MODIFIED_INFO), null);
299         updateHalProgramInfo(false, Arrays.asList(TEST_DAB_INFO), null);
300         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], false,
301                 Arrays.asList(TEST_DAB_INFO), null);
302 
303         // Verify closing the AIDL session also stops HAL updates.
304         mTunerSessions[0].close();
305         verify(mHalTunerSessionMock).stopProgramListUpdates();
306     }
307 
openAidlClients(int numClients)308     private void openAidlClients(int numClients) throws RemoteException {
309         mAidlTunerCallbackMocks = new android.hardware.radio.ITunerCallback[numClients];
310         mTunerSessions = new TunerSession[numClients];
311         for (int i = 0; i < numClients; i++) {
312             mAidlTunerCallbackMocks[i] = mock(android.hardware.radio.ITunerCallback.class);
313             mTunerSessions[i] = mRadioModule.openSession(mAidlTunerCallbackMocks[i]);
314         }
315     }
316 
updateHalProgramInfo(boolean purge, List<RadioManager.ProgramInfo> modified, List<ProgramSelector.Identifier> removed)317     private void updateHalProgramInfo(boolean purge, List<RadioManager.ProgramInfo> modified,
318             List<ProgramSelector.Identifier> removed) throws RemoteException {
319         ProgramListChunk programListChunk = new ProgramListChunk();
320         programListChunk.purge = purge;
321         programListChunk.complete = true;
322         if (modified != null) {
323             for (RadioManager.ProgramInfo mod : modified) {
324                 programListChunk.modified.add(TestUtils.programInfoToHal(mod));
325             }
326         }
327         if (removed != null) {
328             for (ProgramSelector.Identifier id : removed) {
329                 programListChunk.removed.add(Convert.programIdentifierToHal(id));
330             }
331         }
332         mHalTunerCallback.onProgramListUpdated(programListChunk);
333     }
334 
verifyAidlClientReceivedChunk(android.hardware.radio.ITunerCallback clientMock, boolean purge, List<RadioManager.ProgramInfo> modified, List<UniqueProgramIdentifier> removed)335     private void verifyAidlClientReceivedChunk(android.hardware.radio.ITunerCallback clientMock,
336             boolean purge, List<RadioManager.ProgramInfo> modified,
337             List<UniqueProgramIdentifier> removed) throws RemoteException {
338         HashSet<RadioManager.ProgramInfo> modifiedSet = new HashSet<>();
339         if (modified != null) {
340             modifiedSet.addAll(modified);
341         }
342         HashSet<UniqueProgramIdentifier> removedSet = new HashSet<>();
343         if (removed != null) {
344             removedSet.addAll(removed);
345         }
346         ProgramList.Chunk expectedChunk = new ProgramList.Chunk(purge, true, modifiedSet,
347                 removedSet);
348         verify(clientMock, CB_TIMEOUT).onProgramListUpdated(expectedChunk);
349     }
350 }
351