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