xref: /aosp_15_r20/cts/tests/tests/broadcastradio/src/android/broadcastradio/cts/RadioTunerTest.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
2  * Copyright (C) 2022 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 
17 package android.broadcastradio.cts;
18 
19 import static com.google.common.truth.TruthJUnit.assume;
20 
21 import static org.junit.Assert.assertThrows;
22 import static org.junit.Assume.assumeFalse;
23 import static org.junit.Assume.assumeTrue;
24 
25 import android.graphics.Bitmap;
26 import android.hardware.radio.Flags;
27 import android.hardware.radio.ProgramList;
28 import android.hardware.radio.ProgramSelector;
29 import android.hardware.radio.RadioAlert;
30 import android.hardware.radio.RadioManager;
31 import android.hardware.radio.RadioMetadata;
32 import android.hardware.radio.RadioTuner;
33 import android.platform.test.annotations.RequiresFlagsEnabled;
34 
35 import androidx.test.ext.junit.runners.AndroidJUnit4;
36 
37 import com.android.compatibility.common.util.ApiTest;
38 
39 import com.google.common.collect.Range;
40 
41 import org.junit.Test;
42 import org.junit.runner.RunWith;
43 
44 import java.util.Collections;
45 import java.util.List;
46 import java.util.Set;
47 import java.util.concurrent.CountDownLatch;
48 import java.util.concurrent.TimeUnit;
49 
50 /**
51  * CTS test for broadcast radio.
52  */
53 @RunWith(AndroidJUnit4.class)
54 public final class RadioTunerTest extends AbstractRadioTestCase {
55 
56     private static final int TEST_CONFIG_FLAG = RadioManager.CONFIG_FORCE_ANALOG;
57 
58     @Test
59     @ApiTest(apis = {"android.hardware.radio.RadioTuner#close"})
close_twice()60     public void close_twice() throws Exception {
61         openAmFmTuner();
62 
63         mRadioTuner.close();
64         mRadioTuner.close();
65 
66         mExpect.withMessage("Error callbacks").that(mCallback.errorCount).isEqualTo(0);
67     }
68 
69     @Test
70     @ApiTest(apis = {"android.hardware.radio.RadioTuner#cancel",
71             "android.hardware.radio.RadioTuner#close"})
cancel_afterTunerCloses_returnsInvalidOperationStatus()72     public void cancel_afterTunerCloses_returnsInvalidOperationStatus() throws Exception {
73         openAmFmTuner();
74         mRadioTuner.close();
75 
76         int cancelResult = mRadioTuner.cancel();
77 
78         mExpect.withMessage("Result of cancel operation after tuner closed")
79                 .that(cancelResult).isEqualTo(RadioManager.STATUS_INVALID_OPERATION);
80     }
81 
82     @Test
83     @ApiTest(apis = {"android.hardware.radio.RadioTuner#getMute"})
getMute_returnsFalse()84     public void getMute_returnsFalse() throws Exception {
85         openAmFmTuner();
86 
87         boolean isMuted = mRadioTuner.getMute();
88 
89         mExpect.withMessage("Mute status of tuner without audio").that(isMuted).isFalse();
90     }
91 
92     @Test
93     @ApiTest(apis = {"android.hardware.radio.RadioTuner#getMute",
94             "android.hardware.radio.RadioTuner#setMute"})
setMute_withTrue_MuteSucceeds()95     public void setMute_withTrue_MuteSucceeds() throws Exception {
96         openAmFmTuner();
97 
98         int result = mRadioTuner.setMute(true);
99 
100         mExpect.withMessage("Result of setting mute with true")
101                 .that(result).isEqualTo(RadioManager.STATUS_OK);
102         mExpect.withMessage("Mute status after setting mute with true")
103                 .that(mRadioTuner.getMute()).isTrue();
104     }
105 
106     @Test
107     @ApiTest(apis = {"android.hardware.radio.RadioTuner#getMute",
108             "android.hardware.radio.RadioTuner#setMute"})
setMute_withFalse_MuteSucceeds()109     public void setMute_withFalse_MuteSucceeds() throws Exception {
110         openAmFmTuner();
111         mRadioTuner.setMute(true);
112 
113         int result = mRadioTuner.setMute(false);
114 
115         mExpect.withMessage("Result of setting mute with false")
116                 .that(result).isEqualTo(RadioManager.STATUS_OK);
117         mExpect.withMessage("Mute status after setting mute with false")
118                 .that(mRadioTuner.getMute()).isFalse();
119     }
120 
121     @Test
122     @ApiTest(apis = {"android.hardware.radio.RadioTuner#setMute"})
setMute_withTunerWithoutAudio_returnsError()123     public void setMute_withTunerWithoutAudio_returnsError() throws Exception {
124         openAmFmTuner(/* withAudio= */ false);
125 
126         int result = mRadioTuner.setMute(false);
127 
128         mExpect.withMessage("Status of setting mute on tuner without audio")
129                 .that(result).isEqualTo(RadioManager.STATUS_ERROR);
130     }
131 
132     @Test
133     @ApiTest(apis = {"android.hardware.radio.RadioTuner#getMute"})
isMuted_withTunerWithoutAudio_returnsTrue()134     public void isMuted_withTunerWithoutAudio_returnsTrue() throws Exception {
135         openAmFmTuner(/* withAudio= */ false);
136 
137         boolean isMuted = mRadioTuner.getMute();
138 
139         mExpect.withMessage("Mute status of tuner without audio").that(isMuted).isTrue();
140     }
141 
142     @Test
143     @ApiTest(apis = {"android.hardware.radio.RadioTuner#step"})
step_withDownDirection()144     public void step_withDownDirection() throws Exception {
145         openAmFmTuner();
146 
147         int stepResult = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ true);
148 
149         mExpect.withMessage("Program info callback invoked for step operation with down direction")
150                 .that(mCallback.waitForProgramInfoChangeCallback(TUNE_CALLBACK_TIMEOUT_MS))
151                 .isTrue();
152         mExpect.withMessage("Result of step operation with down direction")
153                 .that(stepResult).isEqualTo(RadioManager.STATUS_OK);
154     }
155 
156     @Test
157     @ApiTest(apis = {"android.hardware.radio.RadioTuner#step",
158             "android.hardware.radio.RadioTuner.Callback#onProgramInfoChanged"})
step_withUpDirection()159     public void step_withUpDirection() throws Exception {
160         openAmFmTuner();
161 
162         int stepResult = mRadioTuner.step(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ false);
163 
164         mExpect.withMessage("Program info callback for step operation with up direction")
165                 .that(mCallback.waitForProgramInfoChangeCallback(TUNE_CALLBACK_TIMEOUT_MS))
166                 .isTrue();
167         mExpect.withMessage("Result of step operation with up direction")
168                 .that(stepResult).isEqualTo(RadioManager.STATUS_OK);
169     }
170 
171     @Test
172     @ApiTest(apis = {"android.hardware.radio.RadioTuner#step",
173             "android.hardware.radio.RadioTuner.Callback#onProgramInfoChanged"})
step_withMultipleTimes()174     public void step_withMultipleTimes() throws Exception {
175         openAmFmTuner();
176 
177         for (int index = 0; index < 10; index++) {
178             int stepResult =
179                     mRadioTuner.step(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ true);
180 
181             mExpect.withMessage("Program info callback for step operation at iteration %s",
182                     index).that(mCallback.waitForProgramInfoChangeCallback(
183                             TUNE_CALLBACK_TIMEOUT_MS)).isTrue();
184             mExpect.withMessage("Result of step operation at iteration %s", index)
185                     .that(stepResult).isEqualTo(RadioManager.STATUS_OK);
186 
187             assertNoTunerFailureAndResetCallback("step");
188         }
189     }
190 
191     @Test
192     @ApiTest(apis = {"android.hardware.radio.RadioTuner#seek"})
seek_onProgramInfoChangedInvoked()193     public void seek_onProgramInfoChangedInvoked() throws Exception {
194         openAmFmTuner();
195 
196         int seekResult = mRadioTuner.seek(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ true);
197 
198         mExpect.withMessage("Program info callback for seek operation")
199                 .that(mCallback.waitForProgramInfoChangeCallback(TUNE_CALLBACK_TIMEOUT_MS))
200                 .isTrue();
201         mExpect.withMessage("Result of seek operation")
202                 .that(seekResult).isEqualTo(RadioManager.STATUS_OK);
203     }
204 
205     @Test
206     @ApiTest(apis = {"android.hardware.radio.RadioTuner#tune"})
tune_withFmSelector_onProgramInfoChangedInvoked()207     public void tune_withFmSelector_onProgramInfoChangedInvoked() throws Exception {
208         openAmFmTuner();
209         int freq = mFmBandConfig.getLowerLimit() + mFmBandConfig.getSpacing();
210         ProgramSelector sel = ProgramSelector.createAmFmSelector(RadioManager.BAND_FM, freq,
211                 /* subChannel= */ 0);
212 
213         mRadioTuner.tune(sel);
214 
215         mExpect.withMessage("Program info callback for tune operation")
216                 .that(mCallback.waitForProgramInfoChangeCallback(TUNE_CALLBACK_TIMEOUT_MS))
217                 .isTrue();
218         mExpect.withMessage("Program selector tuned to")
219                 .that(mCallback.currentProgramInfo.getSelector()).isEqualTo(sel);
220     }
221 
222     @Test
223     @ApiTest(apis = {"android.hardware.radio.RadioTuner#cancel"})
cancel_afterNoOperations()224     public void cancel_afterNoOperations() throws Exception {
225         openAmFmTuner();
226 
227         int cancelResult = mRadioTuner.cancel();
228 
229         mCallback.waitForProgramInfoChangeCallback(CANCEL_TIMEOUT_MS);
230         mExpect.withMessage("Result of cancel operation after no operations")
231                 .that(cancelResult).isEqualTo(RadioManager.STATUS_OK);
232         mExpect.withMessage("Tuner failure of cancel operation after no operations")
233                 .that(mCallback.tunerFailureResult).isNotEqualTo(RadioTuner.TUNER_RESULT_CANCELED);
234     }
235 
236     @Test
237     @ApiTest(apis = {"android.hardware.radio.RadioTuner#step",
238             "android.hardware.radio.RadioTuner#cancel"})
cancel_afterStepCompletes()239     public void cancel_afterStepCompletes() throws Exception {
240         openAmFmTuner();
241         int stepResult = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false);
242         mExpect.withMessage("Result of step operation")
243                 .that(stepResult).isEqualTo(RadioManager.STATUS_OK);
244         mExpect.withMessage("Program info callback before cancellation")
245                 .that(mCallback.waitForProgramInfoChangeCallback(TUNE_CALLBACK_TIMEOUT_MS))
246                 .isTrue();
247 
248         int cancelResult = mRadioTuner.cancel();
249 
250         mCallback.waitForProgramInfoChangeCallback(CANCEL_TIMEOUT_MS);
251         mExpect.withMessage("Result of cancel operation after step completed")
252                 .that(cancelResult).isEqualTo(RadioManager.STATUS_OK);
253         mExpect.withMessage("Tuner failure of cancel operation after step completed")
254                 .that(mCallback.tunerFailureResult).isNotEqualTo(RadioTuner.TUNER_RESULT_CANCELED);
255     }
256 
257     @Test
258     @ApiTest(apis = {"android.hardware.radio.RadioTuner#getDynamicProgramList"})
getDynamicProgramList_programListUpdated()259     public void getDynamicProgramList_programListUpdated() throws Exception {
260         openAmFmTuner();
261         TestOnCompleteListener completeListener = new TestOnCompleteListener();
262 
263         ProgramList list = assumeNonNullProgramList();
264         try {
265             list.addOnCompleteListener(completeListener);
266 
267             mExpect.withMessage("List update completion").that(completeListener.waitForCallback())
268                     .isTrue();
269         } finally {
270             list.close();
271         }
272     }
273 
274     @Test
275     @ApiTest(apis = {"android.hardware.radio.ProgramList#getProgramInfos"})
276     @RequiresFlagsEnabled(Flags.FLAG_HD_RADIO_IMPROVED)
getProgramInfos_fromDynamicProgramList()277     public void getProgramInfos_fromDynamicProgramList() throws Exception {
278         openAmFmTuner();
279         TestOnCompleteListener completeListener = new TestOnCompleteListener();
280         ProgramList list = assumeNonNullProgramList();
281         try {
282             list.addOnCompleteListener(completeListener);
283             mExpect.withMessage("List update completion before getting program info")
284                     .that(completeListener.waitForCallback()).isTrue();
285             List<RadioManager.ProgramInfo> programInfoList = list.toList();
286             assume().withMessage("Non-empty program program list")
287                     .that(programInfoList).isNotEmpty();
288             RadioManager.ProgramInfo firstProgram = programInfoList.get(0);
289 
290             mExpect.withMessage("Program list infos")
291                     .that(list.getProgramInfos(firstProgram.getSelector().getPrimaryId()))
292                     .contains(firstProgram);
293         } finally {
294             list.close();
295         }
296     }
297 
298     @Test
299     @ApiTest(apis = {"android.hardware.radio.RadioManager.ProgramInfo#getAlert",
300             "android.hardware.radio.RadioAlert#getMessageType",
301             "android.hardware.radio.RadioAlert#getStatus",
302             "android.hardware.radio.RadioAlert#getInfoList",
303             "android.hardware.radio.RadioAlert.AlertInfo#getCategories",
304             "android.hardware.radio.RadioAlert.AlertInfo#getUrgency",
305             "android.hardware.radio.RadioAlert.AlertInfo#getSeverity",
306             "android.hardware.radio.RadioAlert.AlertInfo#getDescription",
307             "android.hardware.radio.RadioAlert.AlertInfo#getLanguage",
308             "android.hardware.radio.RadioAlert.AlertInfo#getAreas",
309             "android.hardware.radio.RadioAlert.AlertArea#getGeocodes",
310             "android.hardware.radio.RadioAlert.AlertArea#getPolygons",
311             "android.hardware.radio.RadioAlert.Geocode#getValueName",
312             "android.hardware.radio.RadioAlert.Geocode#getValue",
313             "android.hardware.radio.RadioAlert.Polygon#getCoordinates",
314             "android.hardware.radio.RadioAlert.Coordinate#getLatitude",
315             "android.hardware.radio.RadioAlert.Coordinate#getLongitude"})
316     @RequiresFlagsEnabled(Flags.FLAG_HD_RADIO_EMERGENCY_ALERT_SYSTEM)
getAlert_forDynamicProgramListAndCurrentProgramInfo()317     public void getAlert_forDynamicProgramListAndCurrentProgramInfo() throws Exception {
318         openAmFmTuner();
319         TestOnCompleteListener completeListener = new TestOnCompleteListener();
320         ProgramList list = assumeNonNullProgramList();
321         try {
322             list.addOnCompleteListener(completeListener);
323             mExpect.withMessage("List update completion before getting program info")
324                     .that(completeListener.waitForCallback()).isTrue();
325             List<RadioManager.ProgramInfo> programInfoList = list.toList();
326             int alertCount = 0;
327 
328             for (int i = 0; i < programInfoList.size(); i++) {
329                 RadioAlert alert = programInfoList.get(i).getAlert();
330                 if (alert == null) {
331                     continue;
332                 }
333                 alertCount++;
334                 verifyRadioAlert(alert);
335             }
336             if (mCallback.currentProgramInfo != null
337                     && mCallback.currentProgramInfo.getAlert() != null) {
338                 verifyRadioAlert(mCallback.currentProgramInfo.getAlert());
339                 alertCount++;
340             }
341             assume().withMessage("Non-null alert in program list and current program info")
342                     .that(alertCount).isNotEqualTo(0);
343         } finally {
344             list.close();
345         }
346     }
347 
348     @Test
349     @ApiTest(apis = {"android.hardware.radio.RadioTuner#getDynamicProgramList",
350             "android.hardware.radio.RadioTuner#tune"})
tune_withHdSelectorFromDynamicProgramList()351     public void tune_withHdSelectorFromDynamicProgramList() throws Exception {
352         ProgramList.Filter hdFilter = new ProgramList.Filter(
353                 Set.of(ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT), Collections.emptySet(),
354                 /* includeCategories= */ true, /* excludeModifications= */ false);
355         openAmFmTuner();
356         TestOnCompleteListener completeListener = new TestOnCompleteListener();
357         ProgramList list = assumeNonNullFiltereredDynamicProgramList(hdFilter);
358         try {
359             list.addOnCompleteListener(completeListener);
360             mExpect.withMessage("HD program list update completion")
361                     .that(completeListener.waitForCallback()).isTrue();
362             List<RadioManager.ProgramInfo> programInfoList = list.toList();
363             assume().withMessage("Non-empty HD radio program program list")
364                     .that(programInfoList).isNotEmpty();
365             ProgramSelector firstHdSelector = programInfoList.get(0).getSelector();
366 
367             mRadioTuner.tune(firstHdSelector);
368 
369             mExpect.withMessage("Program info callback for HD tune operation")
370                     .that(mCallback.waitForProgramInfoChangeCallback(TUNE_CALLBACK_TIMEOUT_MS))
371                     .isTrue();
372             mExpect.withMessage("HD program selector tuned to")
373                     .that(mCallback.currentProgramInfo.getSelector()).isEqualTo(firstHdSelector);
374         } finally {
375             list.close();
376         }
377     }
378 
379     @Test
380     @ApiTest(apis = {"android.hardware.radio.RadioMetadata#getBitmapId",
381             "android.hardware.radio.RadioTuner#getMetadataImage"})
382     @RequiresFlagsEnabled(Flags.FLAG_HD_RADIO_IMPROVED)
getMetadataImage()383     public void getMetadataImage() throws Exception {
384         openAmFmTuner();
385         TestOnCompleteListener completeListener = new TestOnCompleteListener();
386         try (ProgramList list = assumeNonNullProgramList()) {
387             list.addOnCompleteListener(completeListener);
388             mExpect.withMessage("Program list update completion")
389                     .that(completeListener.waitForCallback()).isTrue();
390             ProgramSelector selector = getSelectorWithBitmap(list.toList());
391             assume().withMessage("Program selector of program info with bitmap key in program list")
392                     .that(selector).isNotNull();
393             mRadioTuner.tune(selector);
394             mExpect.withMessage("Program info callback for tune operation")
395                     .that(mCallback.waitForProgramInfoChangeCallback(TUNE_CALLBACK_TIMEOUT_MS))
396                     .isTrue();
397             RadioManager.ProgramInfo currentInfo = mCallback.currentProgramInfo;
398             mCallback.reset();
399             int bitmapId = assumeImageMetadataId(currentInfo);
400 
401             Bitmap bitmap = mRadioTuner.getMetadataImage(bitmapId);
402 
403             mExpect.withMessage("Bitmap metadata").that(bitmap).isNotNull();
404         } catch (IllegalArgumentException e) {
405             mExpect.withMessage("Exception for bitmap unavailable").that(e)
406                     .hasMessageThat().contains("not available");
407             mExpect.withMessage("Bitmap metadata without updated from tuner")
408                     .that(mCallback.currentProgramInfo).isNotNull();
409         }
410     }
411 
412     @Test
413     @ApiTest(apis = {"android.hardware.radio.RadioTuner#isConfigFlagSupported",
414             "android.hardware.radio.RadioTuner#isConfigFlagSet"})
isConfigFlagSet_forTunerNotSupported_throwsException()415     public void isConfigFlagSet_forTunerNotSupported_throwsException() throws Exception {
416         openAmFmTuner();
417         boolean isSupported = mRadioTuner.isConfigFlagSupported(TEST_CONFIG_FLAG);
418         assumeFalse("Config flag supported", isSupported);
419 
420         assertThrows(UnsupportedOperationException.class,
421                 () -> mRadioTuner.isConfigFlagSet(TEST_CONFIG_FLAG));
422     }
423 
424     @Test
425     @ApiTest(apis = {"android.hardware.radio.RadioTuner#setConfigFlag",
426             "android.hardware.radio.RadioTuner#setConfigFlag"})
setConfigFlag_forTunerNotSupported_throwsException()427     public void setConfigFlag_forTunerNotSupported_throwsException() throws Exception {
428         openAmFmTuner();
429         boolean isSupported = mRadioTuner.isConfigFlagSupported(TEST_CONFIG_FLAG);
430         assumeFalse("Config flag supported", isSupported);
431 
432         assertThrows(UnsupportedOperationException.class,
433                 () -> mRadioTuner.setConfigFlag(TEST_CONFIG_FLAG, /* value= */ true));
434     }
435 
436     @Test
437     @ApiTest(apis = {"android.hardware.radio.RadioTuner#setConfigFlag",
438             "android.hardware.radio.RadioTuner#setConfigFlag",
439             "android.hardware.radio.RadioTuner#isConfigFlagSet"})
setConfigFlag_withTrue_succeeds()440     public void setConfigFlag_withTrue_succeeds() throws Exception {
441         openAmFmTuner();
442         boolean isSupported = mRadioTuner.isConfigFlagSupported(TEST_CONFIG_FLAG);
443         assumeTrue("Config flag supported", isSupported);
444 
445         mRadioTuner.setConfigFlag(TEST_CONFIG_FLAG, /* value= */ true);
446 
447         mExpect.withMessage("Config flag with true value")
448                 .that(mRadioTuner.isConfigFlagSet(TEST_CONFIG_FLAG)).isTrue();
449         mExpect.withMessage("Config flag callback count when setting true config flag value")
450                 .that(mCallback.configFlagCount).isEqualTo(0);
451     }
452 
453     @Test
454     @ApiTest(apis = {"android.hardware.radio.RadioTuner#setConfigFlag",
455             "android.hardware.radio.RadioTuner#setConfigFlag",
456             "android.hardware.radio.RadioTuner#isConfigFlagSet"})
setConfigFlag_withFalse_succeeds()457     public void setConfigFlag_withFalse_succeeds() throws Exception {
458         openAmFmTuner();
459         boolean isSupported = mRadioTuner.isConfigFlagSupported(TEST_CONFIG_FLAG);
460         assumeTrue("Config flag supported", isSupported);
461 
462         mRadioTuner.setConfigFlag(TEST_CONFIG_FLAG, /* value= */ false);
463 
464         mExpect.withMessage("Config flag with false value")
465                 .that(mRadioTuner.isConfigFlagSet(TEST_CONFIG_FLAG)).isFalse();
466         mExpect.withMessage("Config flag callback count when setting false config flag value")
467                 .that(mCallback.configFlagCount).isEqualTo(0);
468     }
469 
assumeNonNullProgramList()470     private ProgramList assumeNonNullProgramList() {
471         return assumeNonNullFiltereredDynamicProgramList(/* filter= */ null);
472     }
473 
assumeNonNullFiltereredDynamicProgramList(ProgramList.Filter filter)474     private ProgramList assumeNonNullFiltereredDynamicProgramList(ProgramList.Filter filter) {
475         ProgramList list = mRadioTuner.getDynamicProgramList(filter);
476         assume().withMessage("Dynamic program list supported").that(list).isNotNull();
477         return list;
478     }
479 
getSelectorWithBitmap(List<RadioManager.ProgramInfo> programInfoList)480     private ProgramSelector getSelectorWithBitmap(List<RadioManager.ProgramInfo> programInfoList) {
481         for (int i = 0; i < programInfoList.size(); i++) {
482             RadioMetadata metadata = programInfoList.get(i).getMetadata();
483             int iconId = metadata.getBitmapId(RadioMetadata.METADATA_KEY_ICON);
484             if (iconId != 0) {
485                 return programInfoList.get(i).getSelector();
486             }
487             int artId = metadata.getBitmapId(RadioMetadata.METADATA_KEY_ART);
488             if (artId != 0) {
489                 return programInfoList.get(i).getSelector();
490             }
491         }
492         return null;
493     }
494 
assumeImageMetadataId(RadioManager.ProgramInfo info)495     private int assumeImageMetadataId(RadioManager.ProgramInfo info) {
496         RadioMetadata metadata = info.getMetadata();
497         int iconId = metadata.getBitmapId(RadioMetadata.METADATA_KEY_ICON);
498         int artId = metadata.getBitmapId(RadioMetadata.METADATA_KEY_ART);
499         assumeTrue("Bitmap id not supported", iconId != 0 || artId != 0);
500         if (iconId != 0) {
501             return iconId;
502         } else {
503             return artId;
504         }
505     }
506 
verifyRadioAlert(RadioAlert alert)507     private void verifyRadioAlert(RadioAlert alert) {
508         mExpect.withMessage("Alert message type").that(alert.getMessageType())
509                 .isIn(Range.closed(RadioAlert.MESSAGE_TYPE_ALERT,
510                         RadioAlert.MESSAGE_TYPE_CANCEL));
511         mExpect.withMessage("Alert status").that(alert.getStatus())
512                 .isIn(Range.closed(RadioAlert.STATUS_ACTUAL, RadioAlert.STATUS_TEST));
513         List<RadioAlert.AlertInfo> alertInfos = alert.getInfoList();
514         for (int infoIdx = 0; infoIdx < alertInfos.size(); infoIdx++) {
515             RadioAlert.AlertInfo alertInfo = alertInfos.get(infoIdx);
516             int[] categories = alertInfo.getCategories();
517             mExpect.withMessage("Non-empty category array")
518                     .that(categories.length).isNotEqualTo(0);
519             for (int categoryIdx = 0; categoryIdx < categories.length; categoryIdx++) {
520                 mExpect.withMessage("Alert category").that(categories[categoryIdx])
521                         .isIn(Range.closed(RadioAlert.CERTAINTY_OBSERVED,
522                                 RadioAlert.CATEGORY_OTHER));
523             }
524             mExpect.withMessage("Alert urgency").that(alertInfo.getUrgency())
525                     .isIn(Range.closed(RadioAlert.URGENCY_IMMEDIATE,
526                             RadioAlert.URGENCY_UNKNOWN));
527             mExpect.withMessage("Alert severity").that(alertInfo.getSeverity())
528                     .isIn(Range.closed(RadioAlert.SEVERITY_EXTREME,
529                             RadioAlert.SEVERITY_UNKNOWN));
530             mExpect.withMessage("Alert certainty").that(alertInfo.getCertainty())
531                     .isIn(Range.closed(RadioAlert.CERTAINTY_OBSERVED,
532                             RadioAlert.CERTAINTY_UNKNOWN));
533             String alertLanguage = alertInfo.getLanguage();
534             mExpect.withMessage("Alert description").that(alertInfo.toString())
535                     .contains(alertInfo.getDescription());
536             if (alertLanguage != null) {
537                 mExpect.withMessage("Non-empty alert language").that(alertLanguage)
538                         .isNotEmpty();
539             }
540             List<RadioAlert.AlertArea> alertAreas = alertInfo.getAreas();
541             for (int areaIdx = 0; areaIdx < alertAreas.size(); areaIdx++) {
542                 RadioAlert.AlertArea alertArea = alertAreas.get(areaIdx);
543                 for (int geocodeIdx = 0; geocodeIdx < alertArea.getGeocodes().size();
544                         geocodeIdx++) {
545                     mExpect.withMessage("Non-empty geocode value name").that(alertArea
546                             .getGeocodes().get(geocodeIdx).getValueName()).isNotEmpty();
547                     mExpect.withMessage("Non-empty geocode value").that(alertArea
548                             .getGeocodes().get(geocodeIdx).getValue()).isNotEmpty();
549                 }
550                 for (int polygonIdx = 0; polygonIdx < alertArea.getPolygons().size();
551                         polygonIdx++) {
552                     List<RadioAlert.Coordinate> coordinates = alertArea.getPolygons()
553                             .get(polygonIdx).getCoordinates();
554                     mExpect.withMessage("Polygon coordinate counts")
555                             .that(coordinates.size()).isAtLeast(4);
556                     mExpect.withMessage("Latitude for the first and last coordinate")
557                             .that(coordinates.getFirst().getLatitude())
558                             .isEqualTo(coordinates.getLast().getLatitude());
559                     mExpect.withMessage("Longitude for the first and last coordinate")
560                             .that(coordinates.getFirst().getLongitude())
561                             .isEqualTo(coordinates.getLast().getLongitude());
562                     for (int coordinateIdx = 0; coordinateIdx < coordinates.size();
563                             coordinateIdx++) {
564                         mExpect.withMessage("Latitude of polygon area")
565                                 .that(coordinates.get(coordinateIdx).getLatitude())
566                                 .isIn(Range.closed(-90.0, 90.0));
567                         mExpect.withMessage("Longitude of polygon area")
568                                 .that(coordinates.get(coordinateIdx).getLongitude())
569                                 .isIn(Range.closed(-180.0, 180.0));
570                     }
571                 }
572             }
573         }
574     }
575 
576     private static final class TestOnCompleteListener implements ProgramList.OnCompleteListener {
577 
578         private final CountDownLatch mOnCompleteLatch = new CountDownLatch(1);
579 
waitForCallback()580         public boolean waitForCallback() throws InterruptedException {
581             return mOnCompleteLatch.await(PROGRAM_LIST_COMPLETE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
582         }
583 
584         @Override
onComplete()585         public void onComplete() {
586             mOnCompleteLatch.countDown();
587         }
588 
589     }
590 }
591