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

Commit bbe4064c authored by Weilin Xu's avatar Weilin Xu Committed by Android (Google) Code Review
Browse files

Merge "Add unit tests for radio program list"

parents e7f85a7c 1810a320
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);