blob: d7b48be73d5f96246d4edf297d3ba7f6612ac74d [file] [log] [blame]
/*
* Copyright (C) 2021 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 com.android.car.audio;
import static android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
import static android.media.AudioAttributes.USAGE_MEDIA;
import static android.media.AudioManager.AUDIOFOCUS_GAIN;
import static android.media.AudioManager.AUDIOFOCUS_REQUEST_DELAYED;
import static android.media.AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
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.eq;
import static org.mockito.Mockito.description;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.car.media.CarAudioManager;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioFocusInfo;
import android.media.AudioManager;
import android.media.audiopolicy.AudioPolicy;
import android.os.Build;
import android.os.Bundle;
import android.util.SparseArray;
import com.android.car.CarLocalServices;
import com.android.car.oem.CarOemProxyService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.List;
@RunWith(MockitoJUnitRunner.class)
public final class CarZonesAudioFocusUnitTest {
private static final String CLIENT_ID = "media-client-id";
private static final String PACKAGE_NAME = "com.android.car.audio";
private static final int AUDIOFOCUS_FLAG = 0;
private static final int PRIMARY_ZONE_ID = CarAudioManager.PRIMARY_AUDIO_ZONE;
private static final int SECONDARY_ZONE_ID = CarAudioManager.PRIMARY_AUDIO_ZONE + 1;
private static final int CLIENT_UID = 1086753;
private final SparseArray<CarAudioFocus> mFocusMocks = generateMockFocus();
private final SparseArray<CarAudioZone> mMockZones = generateAudioZones();
@Mock
private AudioManager mMockAudioManager;
@Mock
private AudioPolicy mAudioPolicy;
@Mock
private CarAudioService mCarAudioService;
@Mock
private CarAudioSettings mCarAudioSettings;
@Mock
private CarZonesAudioFocus.CarFocusCallback mMockCarFocusCallback;
@Mock
private PackageManager mMockPackageManager;
@Mock
private CarOemProxyService mMockCarOemProxyService;
@Mock
private CarVolumeInfoWrapper mMockCarVolumeInfoWrapper;
private CarZonesAudioFocus mCarZonesAudioFocus;
@Before
public void setUp() {
mCarZonesAudioFocus = getCarZonesAudioFocus();
CarLocalServices.removeServiceForTest(CarOemProxyService.class);
CarLocalServices.addService(CarOemProxyService.class, mMockCarOemProxyService);
}
@After
public void tearDown() {
CarLocalServices.removeServiceForTest(CarOemProxyService.class);
}
@Test
public void newCarZonesAudioFocus_withNullAudioManager_throws() {
NullPointerException thrown = assertThrows(NullPointerException.class,
() -> CarZonesAudioFocus.createCarZonesAudioFocus(null,
mMockPackageManager, mMockZones, mCarAudioSettings, mMockCarFocusCallback,
mMockCarVolumeInfoWrapper)
);
assertWithMessage("Create car audio zone with null audio manager exception")
.that(thrown).hasMessageThat().contains("Audio manager");
}
@Test
public void newCarZonesAudioFocus_withNullPackageManager_throws() {
NullPointerException thrown = assertThrows(NullPointerException.class,
() -> CarZonesAudioFocus.createCarZonesAudioFocus(mMockAudioManager,
null, mMockZones, mCarAudioSettings, mMockCarFocusCallback,
mMockCarVolumeInfoWrapper)
);
assertWithMessage("Create car audio zone with null package manager exception")
.that(thrown).hasMessageThat().contains("Package manager");
}
@Test
public void newCarZonesAudioFocus_withNullCarAudioZones_throws() {
NullPointerException thrown = assertThrows(NullPointerException.class,
() -> CarZonesAudioFocus.createCarZonesAudioFocus(mMockAudioManager,
mMockPackageManager, null, mCarAudioSettings, mMockCarFocusCallback,
mMockCarVolumeInfoWrapper)
);
assertWithMessage("Create car audio zone with null zones exception")
.that(thrown).hasMessageThat().contains("Car audio zones");
}
@Test
public void newCarZonesAudioFocus_withEmptyCarAudioZones_throws() {
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
() -> CarZonesAudioFocus.createCarZonesAudioFocus(mMockAudioManager,
mMockPackageManager, new SparseArray<>(), mCarAudioSettings,
mMockCarFocusCallback, mMockCarVolumeInfoWrapper)
);
assertWithMessage("Create car audio zone with no audio zones exception")
.that(thrown).hasMessageThat().contains("minimum of one audio zone");
}
@Test
public void newCarZonesAudioFocus_withNullCarAudioSettings_throws() {
NullPointerException thrown = assertThrows(NullPointerException.class,
() -> CarZonesAudioFocus.createCarZonesAudioFocus(mMockAudioManager,
mMockPackageManager, mMockZones, null, mMockCarFocusCallback,
mMockCarVolumeInfoWrapper)
);
assertWithMessage("Create car audio zone with null car settings exception")
.that(thrown).hasMessageThat().contains("Car audio settings");
}
@Test
public void newCarZonesAudioFocus_withNullCarFocusCallback_succeeds() {
CarZonesAudioFocus.createCarZonesAudioFocus(mMockAudioManager, mMockPackageManager,
mMockZones, mCarAudioSettings, null, mMockCarVolumeInfoWrapper);
}
@Test
public void newCarZonesAudioFocus_withNullCarVolumeInfo_succeeds() {
NullPointerException thrown = assertThrows(NullPointerException.class,
() -> CarZonesAudioFocus.createCarZonesAudioFocus(mMockAudioManager,
mMockPackageManager, mMockZones, mCarAudioSettings, mMockCarFocusCallback,
/* carVolumeInfo= */ null)
);
assertWithMessage("Create car audio zone with null car volume exception")
.that(thrown).hasMessageThat().contains("Car volume info");
}
@Test
public void onAudioFocusRequest_withPrimaryZoneUid_passesRequestToPrimaryZone() {
withUidRoutingToZone(PRIMARY_ZONE_ID);
AudioFocusInfo audioFocusInfo = generateAudioFocusRequest();
mCarZonesAudioFocus.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);
verify(mFocusMocks.get(PRIMARY_ZONE_ID))
.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);
}
@Test
public void onAudioFocusRequest_withSecondaryZoneUid_passesRequestToSecondaryZone() {
withUidRoutingToZone(SECONDARY_ZONE_ID);
AudioFocusInfo audioFocusInfo = generateAudioFocusRequest();
mCarZonesAudioFocus.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);
verify(mFocusMocks.get(SECONDARY_ZONE_ID))
.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);
}
@Test
public void onAudioFocusRequest_withValidBundledZoneId_passesRequestToBundledZone() {
withUidRoutingToZone(PRIMARY_ZONE_ID);
when(mCarAudioService.isAudioZoneIdValid(SECONDARY_ZONE_ID)).thenReturn(true);
AudioFocusInfo audioFocusInfo = generateAudioFocusInfoWithBundledZoneId(SECONDARY_ZONE_ID);
mCarZonesAudioFocus.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);
verify(mFocusMocks.get(SECONDARY_ZONE_ID))
.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);
}
@Test
public void onAudioFocusRequest_withInvalidBundledZoneId_passesRequestBasedOnUid() {
int invalidZoneId = -1;
withUidRoutingToZone(PRIMARY_ZONE_ID);
when(mCarAudioService.isAudioZoneIdValid(invalidZoneId)).thenReturn(false);
AudioFocusInfo audioFocusInfo = generateAudioFocusInfoWithBundledZoneId(invalidZoneId);
mCarZonesAudioFocus.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);
verify(mFocusMocks.get(PRIMARY_ZONE_ID))
.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);
}
@Test
public void onAudioFocusRequest_withFocusCallback_callsOnFocusChange() {
List<AudioFocusInfo> focusHolders = List.of(generateAudioFocusRequest());
when(mFocusMocks.get(PRIMARY_ZONE_ID).getAudioFocusHolders()).thenReturn(focusHolders);
AudioFocusInfo audioFocusInfo = generateAudioFocusRequest();
mCarZonesAudioFocus.onAudioFocusRequest(audioFocusInfo, AUDIOFOCUS_REQUEST_GRANTED);
ArgumentCaptor<SparseArray<List<AudioFocusInfo>>> captor =
ArgumentCaptor.forClass(SparseArray.class);
verify(mMockCarFocusCallback)
.onFocusChange(eq(new int[]{PRIMARY_ZONE_ID}), captor.capture());
SparseArray<List<AudioFocusInfo>> results = captor.getValue();
assertWithMessage("Number of lists returned in sparse array")
.that(results.size()).isEqualTo(1);
assertWithMessage("Focus holders for primary zone")
.that(results.get(PRIMARY_ZONE_ID)).isEqualTo(focusHolders);
}
@Test
public void setRestrictFocus_withTrue_restrictsFocusForAllZones() {
mCarZonesAudioFocus.setRestrictFocus(true);
verify(mFocusMocks.get(PRIMARY_ZONE_ID),
description("Primary zone's CarAudioFocus#setRestrictFocus wasn't passed true"))
.setRestrictFocus(true);
verify(mFocusMocks.get(SECONDARY_ZONE_ID),
description("Secondary zone's CarAudioFocus#setRestrictFocus wasn't passed true"))
.setRestrictFocus(true);
}
@Test
public void setRestrictFocus_withFalse_unrestrictsFocusForAllZones() {
mCarZonesAudioFocus.setRestrictFocus(false);
verify(mFocusMocks.get(PRIMARY_ZONE_ID),
description("Primary zone's CarAudioFocus#setRestrictFocus wasn't passed false"))
.setRestrictFocus(false);
verify(mFocusMocks.get(SECONDARY_ZONE_ID),
description("Secondary zone's CarAudioFocus#setRestrictFocus wasn't passed false"))
.setRestrictFocus(false);
}
@Test
public void setRestrictFocus_notifiesFocusCallbackForAllZones() {
mCarZonesAudioFocus.setRestrictFocus(false);
ArgumentCaptor<SparseArray<List<AudioFocusInfo>>> captor =
ArgumentCaptor.forClass(SparseArray.class);
int[] expectedZoneIds = new int[]{PRIMARY_ZONE_ID, SECONDARY_ZONE_ID};
verify(mMockCarFocusCallback).onFocusChange(eq(expectedZoneIds), captor.capture());
assertWithMessage("Number of focus holder lists")
.that(captor.getValue().size()).isEqualTo(2);
}
@Test
public void transientlyLoseAllFocusHoldersInZone() {
AudioFocusInfo mediaFocusInfo = generateAudioFocusRequest();
AudioFocusInfo navigationFocusInfo =
generateAudioFocusRequestWithUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE);
List<AudioFocusInfo> expectedFocusInfoList = List.of(mediaFocusInfo, navigationFocusInfo);
when(mFocusMocks.get(SECONDARY_ZONE_ID).getAudioFocusHolders())
.thenReturn(expectedFocusInfoList);
List<AudioFocusInfo> audioFocusInfos =
mCarZonesAudioFocus.transientlyLoseAllFocusHoldersInZone(SECONDARY_ZONE_ID);
verify(mFocusMocks.get(SECONDARY_ZONE_ID))
.removeAudioFocusInfoAndTransientlyLoseFocus(mediaFocusInfo);
verify(mFocusMocks.get(SECONDARY_ZONE_ID))
.removeAudioFocusInfoAndTransientlyLoseFocus(navigationFocusInfo);
assertWithMessage("Focus holders in secondary zone")
.that(audioFocusInfos).containsExactlyElementsIn(expectedFocusInfoList);
}
@Test
public void reevaluateAndRegainAudioFocusList_regainsFocus() {
AudioFocusInfo mediaFocusInfo = generateAudioFocusRequest();
AudioFocusInfo navigationFocusInfo =
generateAudioFocusRequestWithUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE);
int mediaFocusRequestResult = AUDIOFOCUS_REQUEST_DELAYED;
int navigationFocusRequestResult = AUDIOFOCUS_REQUEST_GRANTED;
when(mCarAudioService.getZoneIdForAudioFocusInfo(mediaFocusInfo))
.thenReturn(PRIMARY_ZONE_ID);
when(mCarAudioService.getZoneIdForAudioFocusInfo(navigationFocusInfo))
.thenReturn(SECONDARY_ZONE_ID);
when(mFocusMocks.get(PRIMARY_ZONE_ID).reevaluateAndRegainAudioFocus(any()))
.thenReturn(mediaFocusRequestResult);
when(mFocusMocks.get(SECONDARY_ZONE_ID).reevaluateAndRegainAudioFocus(any()))
.thenReturn(navigationFocusRequestResult);
List<Integer> resList = mCarZonesAudioFocus.reevaluateAndRegainAudioFocusList(
List.of(mediaFocusInfo, navigationFocusInfo));
assertWithMessage("Result list size").that(resList.size()).isEqualTo(2);
assertWithMessage("Results for regaining media focus in primary zone")
.that(resList.get(0)).isEqualTo(mediaFocusRequestResult);
assertWithMessage("Results for regaining navigation focus in primary zone")
.that(resList.get(1)).isEqualTo(navigationFocusRequestResult);
}
@Test
public void reevaluateAndRegainAudioFocusList_notifiesFocusListener() {
AudioFocusInfo mediaFocusInfo = generateAudioFocusRequest();
AudioFocusInfo navigationFocusInfo =
generateAudioFocusRequestWithUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE);
when(mCarAudioService.getZoneIdForAudioFocusInfo(mediaFocusInfo))
.thenReturn(PRIMARY_ZONE_ID);
when(mCarAudioService.getZoneIdForAudioFocusInfo(navigationFocusInfo))
.thenReturn(SECONDARY_ZONE_ID);
when(mFocusMocks.get(PRIMARY_ZONE_ID).getAudioFocusHolders())
.thenReturn(List.of(mediaFocusInfo));
when(mFocusMocks.get(SECONDARY_ZONE_ID).getAudioFocusHolders())
.thenReturn(List.of(navigationFocusInfo));
mCarZonesAudioFocus.reevaluateAndRegainAudioFocusList(List.of(mediaFocusInfo,
navigationFocusInfo));
ArgumentCaptor<SparseArray<List<AudioFocusInfo>>> captor =
ArgumentCaptor.forClass(SparseArray.class);
verify(mMockCarFocusCallback).onFocusChange(
eq(new int[]{PRIMARY_ZONE_ID, SECONDARY_ZONE_ID}), captor.capture());
SparseArray<List<AudioFocusInfo>> results = captor.getValue();
assertWithMessage("Zones notified for focus changed")
.that(results.size()).isEqualTo(2);
assertWithMessage("Primary zone focus holders")
.that(results.get(PRIMARY_ZONE_ID)).containsExactly(mediaFocusInfo);
assertWithMessage("Secondary zone focus holders")
.that(results.get(SECONDARY_ZONE_ID)).containsExactly(navigationFocusInfo);
}
@Test
public void transientlyLoseAudioFocusForZone_forActiveFocusHolders() {
AudioFocusInfo mediaFocusInfo = generateAudioFocusRequest();
AudioFocusInfo navigationFocusInfo =
generateAudioFocusRequestWithUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE);
List<AudioFocusInfo> expectedFocusInfoList = List.of(mediaFocusInfo, navigationFocusInfo);
when(mFocusMocks.get(SECONDARY_ZONE_ID).getAudioFocusHolders())
.thenReturn(expectedFocusInfoList);
when(mFocusMocks.get(SECONDARY_ZONE_ID).getAudioFocusLosers()).thenReturn(List.of());
AudioFocusStack stack =
mCarZonesAudioFocus.transientlyLoseAudioFocusForZone(SECONDARY_ZONE_ID);
verify(mFocusMocks.get(SECONDARY_ZONE_ID))
.removeAudioFocusInfoAndTransientlyLoseFocus(mediaFocusInfo);
verify(mFocusMocks.get(SECONDARY_ZONE_ID))
.removeAudioFocusInfoAndTransientlyLoseFocus(navigationFocusInfo);
assertWithMessage("Stack active focus in secondary zone")
.that(stack.getActiveFocusList()).containsExactlyElementsIn(expectedFocusInfoList);
assertWithMessage("Empty stack inactive focus in secondary zone")
.that(stack.getInactiveFocusList()).isEmpty();
}
@Test
public void transientlyLoseAudioFocusForZone_forInactiveFocusHolders() {
AudioFocusInfo mediaFocusInfo = generateAudioFocusRequest();
AudioFocusInfo navigationFocusInfo =
generateAudioFocusRequestWithUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE);
List<AudioFocusInfo> expectedFocusInfoList = List.of(mediaFocusInfo, navigationFocusInfo);
when(mFocusMocks.get(SECONDARY_ZONE_ID).getAudioFocusLosers())
.thenReturn(expectedFocusInfoList);
when(mFocusMocks.get(SECONDARY_ZONE_ID).getAudioFocusHolders()).thenReturn(List.of());
AudioFocusStack stack =
mCarZonesAudioFocus.transientlyLoseAudioFocusForZone(SECONDARY_ZONE_ID);
verify(mFocusMocks.get(SECONDARY_ZONE_ID))
.removeAudioFocusInfoAndTransientlyLoseFocus(mediaFocusInfo);
verify(mFocusMocks.get(SECONDARY_ZONE_ID))
.removeAudioFocusInfoAndTransientlyLoseFocus(navigationFocusInfo);
assertWithMessage("Stack inactive focus in secondary zone")
.that(stack.getInactiveFocusList())
.containsExactlyElementsIn(expectedFocusInfoList);
assertWithMessage("Empty stack active focus in secondary zone")
.that(stack.getActiveFocusList()).isEmpty();
}
private static SparseArray<CarAudioZone> generateAudioZones() {
CarAudioContext testCarAudioContext =
new CarAudioContext(CarAudioContext.getAllContextsInfo(),
/* useCoreAudioRouting= */ false);
int zoneConfigId = 0;
CarAudioZoneConfig primaryZoneConfig =
new CarAudioZoneConfig.Builder("Primary zone config",
PRIMARY_ZONE_ID, zoneConfigId, /* isDefault= */ true)
.build();
CarAudioZoneConfig secondaryZoneConfig =
new CarAudioZoneConfig.Builder("Secondary zone config",
SECONDARY_ZONE_ID, zoneConfigId, /* isDefault= */ true)
.build();
CarAudioZone primaryZone = new CarAudioZone(testCarAudioContext, "Primary zone",
PRIMARY_ZONE_ID);
CarAudioZone secondaryZone = new CarAudioZone(testCarAudioContext, "Secondary zone",
SECONDARY_ZONE_ID);
primaryZone.addZoneConfig(primaryZoneConfig);
secondaryZone.addZoneConfig(secondaryZoneConfig);
SparseArray<CarAudioZone> zones = new SparseArray<>();
zones.put(PRIMARY_ZONE_ID, primaryZone);
zones.put(SECONDARY_ZONE_ID, secondaryZone);
return zones;
}
private static SparseArray<CarAudioFocus> generateMockFocus() {
SparseArray<CarAudioFocus> mockFocusZones = new SparseArray<>();
mockFocusZones.put(PRIMARY_ZONE_ID, mock(CarAudioFocus.class));
mockFocusZones.put(SECONDARY_ZONE_ID, mock(CarAudioFocus.class));
return mockFocusZones;
}
private static AudioFocusInfo generateAudioFocusRequest() {
return generateAudioFocusRequestWithUsage(USAGE_MEDIA);
}
private static AudioFocusInfo generateAudioFocusRequestWithUsage(
@AudioAttributes.AttributeSdkUsage int usage) {
AudioAttributes attributes = new AudioAttributes.Builder().setUsage(usage).build();
return generateAudioFocusInfoWithAttributes(attributes);
}
private static AudioFocusInfo generateAudioFocusInfoWithBundledZoneId(int zoneId) {
Bundle bundle = new Bundle();
bundle.putInt(CarAudioManager.AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID, zoneId);
AudioAttributes attributes = new AudioAttributes.Builder()
.setUsage(USAGE_MEDIA)
.addBundle(bundle)
.build();
return generateAudioFocusInfoWithAttributes(attributes);
}
private static AudioFocusInfo generateAudioFocusInfoWithAttributes(AudioAttributes attributes) {
return new AudioFocusInfo(attributes, CLIENT_UID, CLIENT_ID,
PACKAGE_NAME, AUDIOFOCUS_GAIN, AudioManager.AUDIOFOCUS_NONE, AUDIOFOCUS_FLAG,
Build.VERSION.SDK_INT);
}
private CarZonesAudioFocus getCarZonesAudioFocus() {
CarZonesAudioFocus carZonesAudioFocus =
new CarZonesAudioFocus(mFocusMocks, mMockCarFocusCallback);
carZonesAudioFocus.setOwningPolicy(mCarAudioService, mAudioPolicy);
return carZonesAudioFocus;
}
private void withUidRoutingToZone(int zoneId) {
when(mCarAudioService.getZoneIdForAudioFocusInfo(any())).thenReturn(zoneId);
}
}