Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1810a320 authored by Weilin Xu's avatar Weilin Xu
Browse files

Add unit tests for radio program list

Bug: 249602599
Test: atest android.hardware.radio.tests.unittests
Change-Id: Ia52d322ffc365be54096d3220a6b05f3fecbc207
parent bf06bafa
Loading
Loading
Loading
Loading
+494 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 android.hardware.radio.tests.unittests;

import static com.google.common.truth.Truth.assertWithMessage;

import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;

import android.content.Context;
import android.hardware.radio.IRadioService;
import android.hardware.radio.ITuner;
import android.hardware.radio.ITunerCallback;
import android.hardware.radio.ProgramList;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
import android.hardware.radio.RadioMetadata;
import android.hardware.radio.RadioTuner;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.ArraySet;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.verification.VerificationWithTimeout;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;

@RunWith(MockitoJUnitRunner.class)
public final class ProgramListTest {

    private static final int CREATOR_ARRAY_SIZE = 3;
    private static final VerificationWithTimeout CALLBACK_TIMEOUT = timeout(/* millis= */ 500);

    private static final boolean IS_PURGE = false;
    private static final boolean IS_COMPLETE = true;

    private static final boolean INCLUDE_CATEGORIES = true;
    private static final boolean EXCLUDE_MODIFICATIONS = false;

    private static final ProgramSelector.Identifier FM_IDENTIFIER =
            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
                    /* value= */ 94300);
    private static final ProgramSelector.Identifier RDS_IDENTIFIER =
            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_RDS_PI, 15019);
    private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER =
            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
                    /* value= */ 0x10000111);
    private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER =
            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
                    /* value= */ 0x1013);
    private static final RadioManager.ProgramInfo FM_PROGRAM_INFO = createFmProgramInfo(
            createProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, FM_IDENTIFIER));
    private static final RadioManager.ProgramInfo RDS_PROGRAM_INFO = createFmProgramInfo(
            createProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, RDS_IDENTIFIER));

    private static final Set<Integer> FILTER_IDENTIFIER_TYPES = Set.of(
            ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, ProgramSelector.IDENTIFIER_TYPE_RDS_PI);
    private static final Set<ProgramSelector.Identifier> FILTER_IDENTIFIERS = Set.of(FM_IDENTIFIER);

    private static final ProgramList.Chunk FM_RDS_ADD_CHUNK = new ProgramList.Chunk(IS_PURGE,
            IS_COMPLETE, Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
            Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
    private static final ProgramList.Chunk FM_ADD_INCOMPLETE_CHUNK = new ProgramList.Chunk(IS_PURGE,
            /* complete= */ false, Set.of(FM_PROGRAM_INFO), new ArraySet<>());
    private static final ProgramList.Filter TEST_FILTER = new ProgramList.Filter(
            FILTER_IDENTIFIER_TYPES, FILTER_IDENTIFIERS, INCLUDE_CATEGORIES, EXCLUDE_MODIFICATIONS);
    private static final Map<String, String> VENDOR_FILTER = Map.of("testVendorKey1",
            "testVendorValue1", "testVendorKey2", "testVendorValue2");

    private final Executor mExecutor = new Executor() {
        @Override
        public void execute(Runnable command) {
            command.run();
        }
    };

    private RadioTuner mRadioTuner;
    private ITunerCallback mTunerCallback;
    private ProgramList mProgramList;

    private ProgramList.ListCallback[] mListCallbackMocks;
    private ProgramList.OnCompleteListener[] mOnCompleteListenerMocks;
    @Mock
    private IRadioService mRadioServiceMock;
    @Mock
    private Context mContextMock;
    @Mock
    private ITuner mTunerMock;
    @Mock
    private RadioTuner.Callback mTunerCallbackMock;

    @Test
    public void getIdentifierTypes_forFilter() {
        ProgramList.Filter filter = new ProgramList.Filter(FILTER_IDENTIFIER_TYPES,
                FILTER_IDENTIFIERS, INCLUDE_CATEGORIES, EXCLUDE_MODIFICATIONS);

        assertWithMessage("Filtered identifier types").that(filter.getIdentifierTypes())
                .containsExactlyElementsIn(FILTER_IDENTIFIER_TYPES);
    }

    @Test
    public void getIdentifiers_forFilter() {
        ProgramList.Filter filter = new ProgramList.Filter(FILTER_IDENTIFIER_TYPES,
                FILTER_IDENTIFIERS, INCLUDE_CATEGORIES, EXCLUDE_MODIFICATIONS);

        assertWithMessage("Filtered identifiers").that(filter.getIdentifiers())
                .containsExactlyElementsIn(FILTER_IDENTIFIERS);
    }

    @Test
    public void areCategoriesIncluded_forFilter() {
        ProgramList.Filter filter = new ProgramList.Filter(FILTER_IDENTIFIER_TYPES,
                FILTER_IDENTIFIERS, INCLUDE_CATEGORIES, EXCLUDE_MODIFICATIONS);

        assertWithMessage("Filter including categories")
                .that(filter.areCategoriesIncluded()).isEqualTo(INCLUDE_CATEGORIES);
    }

    @Test
    public void areModificationsExcluded_forFilter() {
        ProgramList.Filter filter = new ProgramList.Filter(FILTER_IDENTIFIER_TYPES,
                FILTER_IDENTIFIERS, INCLUDE_CATEGORIES, EXCLUDE_MODIFICATIONS);

        assertWithMessage("Filter excluding modifications")
                .that(filter.areModificationsExcluded()).isEqualTo(EXCLUDE_MODIFICATIONS);
    }

    @Test
    public void getVendorFilter_forFilterWithoutVendorFilter_returnsNull() {
        ProgramList.Filter filter = new ProgramList.Filter(FILTER_IDENTIFIER_TYPES,
                FILTER_IDENTIFIERS, INCLUDE_CATEGORIES, EXCLUDE_MODIFICATIONS);

        assertWithMessage("Filter vendor obtained from filter without vendor filter")
                .that(filter.getVendorFilter()).isNull();
    }

    @Test
    public void getVendorFilter_forFilterWithVendorFilter() {
        ProgramList.Filter vendorFilter = new ProgramList.Filter(VENDOR_FILTER);

        assertWithMessage("Filter vendor obtained from filter with vendor filter")
                .that(vendorFilter.getVendorFilter()).isEqualTo(VENDOR_FILTER);
    }

    @Test
    public void describeContents_forFilter() {
        assertWithMessage("Filter contents").that(TEST_FILTER.describeContents()).isEqualTo(0);
    }

    @Test
    public void hashCode_withTheSameFilters_equals() {
        ProgramList.Filter filterCompared = new ProgramList.Filter(FILTER_IDENTIFIER_TYPES,
                FILTER_IDENTIFIERS, INCLUDE_CATEGORIES, EXCLUDE_MODIFICATIONS);

        assertWithMessage("Hash code of the same filter")
                .that(filterCompared.hashCode()).isEqualTo(TEST_FILTER.hashCode());
    }

    @Test
    public void hashCode_withDifferentFilters_notEquals() {
        ProgramList.Filter filterCompared = new ProgramList.Filter();

        assertWithMessage("Hash code of the different filter")
                .that(filterCompared.hashCode()).isNotEqualTo(TEST_FILTER.hashCode());
    }

    @Test
    public void writeToParcel_forFilter() {
        Parcel parcel = Parcel.obtain();

        TEST_FILTER.writeToParcel(parcel, /* flags= */ 0);
        parcel.setDataPosition(0);

        ProgramList.Filter filterFromParcel =
                ProgramList.Filter.CREATOR.createFromParcel(parcel);
        assertWithMessage("Filter created from parcel")
                .that(filterFromParcel).isEqualTo(TEST_FILTER);
    }

    @Test
    public void newArray_forFilterCreator() {
        ProgramList.Filter[] filters = ProgramList.Filter.CREATOR.newArray(CREATOR_ARRAY_SIZE);

        assertWithMessage("Program filters").that(filters).hasLength(CREATOR_ARRAY_SIZE);
    }

    @Test
    public void isPurge_forChunk() {
        ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
                Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
                Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));

        assertWithMessage("Puring chunk").that(chunk.isPurge()).isEqualTo(IS_PURGE);
    }

    @Test
    public void isComplete_forChunk() {
        ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
                Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
                Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));

        assertWithMessage("Complete chunk").that(chunk.isComplete()).isEqualTo(IS_COMPLETE);
    }

    @Test
    public void getModified_forChunk() {
        ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
                Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
                Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));

        assertWithMessage("Modified program info in chunk")
                .that(chunk.getModified()).containsExactly(FM_PROGRAM_INFO, RDS_PROGRAM_INFO);
    }

    @Test
    public void getRemoved_forChunk() {
        ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
                Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
                Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));

        assertWithMessage("Removed program identifiers in chunk").that(chunk.getRemoved())
                .containsExactly(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER);
    }

    @Test
    public void describeContents_forChunk() {
        assertWithMessage("Chunk contents").that(FM_RDS_ADD_CHUNK.describeContents()).isEqualTo(0);
    }

    @Test
    public void writeToParcel_forChunk() {
        Parcel parcel = Parcel.obtain();

        FM_RDS_ADD_CHUNK.writeToParcel(parcel, /* flags= */ 0);
        parcel.setDataPosition(0);

        ProgramList.Chunk chunkFromParcel =
                ProgramList.Chunk.CREATOR.createFromParcel(parcel);
        assertWithMessage("Chunk created from parcel")
                .that(chunkFromParcel).isEqualTo(FM_RDS_ADD_CHUNK);
    }

    @Test
    public void newArray_forChunkCreator() {
        ProgramList.Chunk[] chunks = ProgramList.Chunk.CREATOR.newArray(CREATOR_ARRAY_SIZE);

        assertWithMessage("Chunks").that(chunks).hasLength(CREATOR_ARRAY_SIZE);
    }

    @Test
    public void getDynamicProgramList_forTunerAdapter() throws Exception {
        createRadioTuner();

        mRadioTuner.getDynamicProgramList(TEST_FILTER);

        verify(mTunerMock).startProgramListUpdates(TEST_FILTER);
    }

    @Test
    public void getDynamicProgramList_forTunerAdapterWithServiceDied_throwsException()
            throws Exception {
        createRadioTuner();
        doThrow(new RemoteException()).when(mTunerMock).startProgramListUpdates(any());

        RuntimeException thrown = assertThrows(RuntimeException.class, () -> {
            mRadioTuner.getDynamicProgramList(TEST_FILTER);
        });

        assertWithMessage("Exception for radio HAL client service died")
                .that(thrown).hasMessageThat().contains("Service died");
    }

    @Test
    public void onProgramListUpdated_withNewIdsAdded_invokesMockedCallbacks() throws Exception {
        createRadioTuner();
        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
        registerListCallbacks(/* numCallbacks= */ 1);
        addOnCompleteListeners(/* numListeners= */ 1);

        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);

        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemChanged(FM_IDENTIFIER);
        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemChanged(RDS_IDENTIFIER);
        verify(mOnCompleteListenerMocks[0], CALLBACK_TIMEOUT).onComplete();
        assertWithMessage("Program info in program list after adding FM and RDS info")
                .that(mProgramList.toList()).containsExactly(FM_PROGRAM_INFO, RDS_PROGRAM_INFO);
    }

    @Test
    public void onProgramListUpdated_withIdsRemoved_invokesMockedCallbacks() throws Exception {
        ProgramList.Chunk fmRemovedChunk = new ProgramList.Chunk(/* purge= */ false,
                /* complete= */ false, new ArraySet<>(), Set.of(FM_IDENTIFIER));
        createRadioTuner();
        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
        registerListCallbacks(/* numCallbacks= */ 1);
        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);

        mTunerCallback.onProgramListUpdated(fmRemovedChunk);

        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemRemoved(FM_IDENTIFIER);
        assertWithMessage("Program info in program list after removing FM id")
                .that(mProgramList.toList()).containsExactly(RDS_PROGRAM_INFO);
        assertWithMessage("Program info FM identifier")
                .that(mProgramList.get(RDS_IDENTIFIER)).isEqualTo(RDS_PROGRAM_INFO);
    }

    @Test
    public void onProgramListUpdated_withIncompleteChunk_notInvokesOnCompleteListener()
            throws Exception {
        createRadioTuner();
        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
        addOnCompleteListeners(/* numListeners= */ 1);

        mTunerCallback.onProgramListUpdated(FM_ADD_INCOMPLETE_CHUNK);

        verify(mOnCompleteListenerMocks[0], CALLBACK_TIMEOUT.times(0)).onComplete();
    }

    @Test
    public void onProgramListUpdated_withPurgeChunk() throws Exception {
        ProgramList.Chunk purgeChunk = new ProgramList.Chunk(/* purge= */ true,
                /* complete= */ true, new ArraySet<>(), new ArraySet<>());
        createRadioTuner();
        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
        registerListCallbacks(/* numCallbacks= */ 1);
        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);

        mTunerCallback.onProgramListUpdated(purgeChunk);

        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemRemoved(FM_IDENTIFIER);
        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemRemoved(RDS_IDENTIFIER);
        assertWithMessage("Program list after purge chunk applied")
                .that(mProgramList.toList()).isEmpty();
    }

    @Test
    public void registerListCallback_withExecutor() throws Exception {
        createRadioTuner();
        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
        ProgramList.ListCallback listCallbackMock = mock(ProgramList.ListCallback.class);

        mProgramList.registerListCallback(mExecutor, listCallbackMock);
        mTunerCallback.onProgramListUpdated(FM_ADD_INCOMPLETE_CHUNK);

        verify(listCallbackMock, CALLBACK_TIMEOUT).onItemChanged(FM_IDENTIFIER);
    }

    @Test
    public void onProgramListUpdated_withMultipleListCallBacks() throws Exception {
        int numCallbacks = 3;
        createRadioTuner();
        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
        registerListCallbacks(numCallbacks);

        mTunerCallback.onProgramListUpdated(FM_ADD_INCOMPLETE_CHUNK);

        for (int index = 0; index < numCallbacks; index++) {
            verify(mListCallbackMocks[index], CALLBACK_TIMEOUT).onItemChanged(FM_IDENTIFIER);
        }
    }

    @Test
    public void unregisterListCallback_withProgramUpdated_notInvokesCallback() throws Exception {
        createRadioTuner();
        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
        registerListCallbacks(/* numCallbacks= */ 1);

        mProgramList.unregisterListCallback(mListCallbackMocks[0]);
        mTunerCallback.onProgramListUpdated(FM_ADD_INCOMPLETE_CHUNK);

        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT.times(0)).onItemChanged(any());
    }

    @Test
    public void addOnCompleteListener_withExecutor() throws Exception {
        createRadioTuner();
        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
        ProgramList.OnCompleteListener onCompleteListenerMock =
                mock(ProgramList.OnCompleteListener.class);

        mProgramList.addOnCompleteListener(mExecutor, onCompleteListenerMock);
        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);

        verify(onCompleteListenerMock, CALLBACK_TIMEOUT).onComplete();
    }

    @Test
    public void onProgramListUpdated_withMultipleOnCompleteListeners() throws Exception {
        int numListeners = 3;
        createRadioTuner();
        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
        addOnCompleteListeners(numListeners);

        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);

        for (int index = 0; index < numListeners; index++) {
            verify(mOnCompleteListenerMocks[index], CALLBACK_TIMEOUT).onComplete();
        }
    }

    @Test
    public void removeOnCompleteListener_withProgramUpdated_notInvokesListener() throws Exception {
        createRadioTuner();
        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
        addOnCompleteListeners(/* numListeners= */ 1);

        mProgramList.removeOnCompleteListener(mOnCompleteListenerMocks[0]);
        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);

        verify(mOnCompleteListenerMocks[0], CALLBACK_TIMEOUT.times(0)).onComplete();
    }

    @Test
    public void close_forProgramList_invokesStopProgramListUpdates() throws Exception {
        createRadioTuner();
        ProgramList programList = mRadioTuner.getDynamicProgramList(TEST_FILTER);

        programList.close();

        verify(mTunerMock, CALLBACK_TIMEOUT).stopProgramListUpdates();
    }

    private static ProgramSelector createProgramSelector(int programType,
            ProgramSelector.Identifier identifier) {
        return new ProgramSelector(programType, identifier, /* secondaryIds= */ null,
                /* vendorIds= */ null);
    }

    private static RadioManager.ProgramInfo createFmProgramInfo(ProgramSelector selector) {
        return new RadioManager.ProgramInfo(selector, selector.getPrimaryId(),
                selector.getPrimaryId(), /* relatedContents= */ null, /* infoFlags= */ 0,
                /* signalQuality= */ 1, new RadioMetadata.Builder().build(),
                /* vendorInfo= */ null);
    }

    private void createRadioTuner() throws Exception {
        RadioManager radioManager = new RadioManager(mContextMock, mRadioServiceMock);
        RadioManager.BandConfig band = new RadioManager.FmBandConfig(
                new RadioManager.FmBandDescriptor(RadioManager.REGION_ITU_1, RadioManager.BAND_FM,
                        /* lowerLimit= */ 87500, /* upperLimit= */ 108000, /* spacing= */ 200,
                        /* stereo= */ true, /* rds= */ false, /* ta= */ false, /* af= */ false,
                        /* es= */ false));

        doAnswer(invocation -> {
            mTunerCallback = (ITunerCallback) invocation.getArguments()[3];
            return mTunerMock;
        }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any());

        mRadioTuner = radioManager.openTuner(/* moduleId= */ 0, band,
                /* withAudio= */ true, mTunerCallbackMock, /* handler= */ null);
    }

    private void registerListCallbacks(int numCallbacks) {
        mListCallbackMocks = new ProgramList.ListCallback[numCallbacks];
        for (int index = 0; index < numCallbacks; index++) {
            mListCallbackMocks[index] = mock(ProgramList.ListCallback.class);
            mProgramList.registerListCallback(mListCallbackMocks[index]);
        }
    }

    private void addOnCompleteListeners(int numListeners) {
        mOnCompleteListenerMocks = new ProgramList.OnCompleteListener[numListeners];
        for (int index = 0; index < numListeners; index++) {
            mOnCompleteListenerMocks[index] = mock(ProgramList.OnCompleteListener.class);
            mProgramList.addOnCompleteListener(mOnCompleteListenerMocks[index]);
        }
    }
}
+10 −0
Original line number Diff line number Diff line
@@ -279,6 +279,16 @@ public final class TunerAdapterTest {
                .that(image).isEqualTo(bitmapExpected);
    }

    @Test
    public void startBackgroundScan_forTunerAdapter() throws Exception {
        when(mTunerMock.startBackgroundScan()).thenReturn(false);

        boolean scanStatus = mRadioTuner.startBackgroundScan();

        verify(mTunerMock).startBackgroundScan();
        assertWithMessage("Status for starting background scan").that(scanStatus).isFalse();
    }

    @Test
    public void isAnalogForced_forTunerAdapter() throws Exception {
        when(mTunerMock.isConfigFlagSet(RadioManager.CONFIG_FORCE_ANALOG)).thenReturn(true);