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

Commit d5026e8c authored by Jean-Michel Trivi's avatar Jean-Michel Trivi
Browse files

SpatializerHelper: fix canBeSpatializedOnDevice NPE

Check whether routing is known when calling
canBeSpatializedOnDevice. Unknown routing is defined
by an empty list of devices for the requested
AudioAttributes.

Add test method in SpatializerHelper to force state.
Implement a unit test covering failure modes when
querying routing.

Test: atest com.android.server.audio.SpatializerHelperTest#testNoRoutingCanBeSpatialized
Bug: 267963974
Change-Id: Ib6d73fec4d57cf372361762b5c1d85329794de92
Merged-In: Ib6d73fec4d57cf372361762b5c1d85329794de92
(cherry picked from commit af85e232)
parent 52bf2033
Loading
Loading
Loading
Loading
+72 −38
Original line number Diff line number Diff line
@@ -107,12 +107,12 @@ public class SpatializerHelper {
    };

    // Spatializer state machine
    private static final int STATE_UNINITIALIZED = 0;
    private static final int STATE_NOT_SUPPORTED = 1;
    private static final int STATE_DISABLED_UNAVAILABLE = 3;
    private static final int STATE_ENABLED_UNAVAILABLE = 4;
    private static final int STATE_ENABLED_AVAILABLE = 5;
    private static final int STATE_DISABLED_AVAILABLE = 6;
    /*package*/ static final int STATE_UNINITIALIZED = 0;
    /*package*/ static final int STATE_NOT_SUPPORTED = 1;
    /*package*/ static final int STATE_DISABLED_UNAVAILABLE = 3;
    /*package*/ static final int STATE_ENABLED_UNAVAILABLE = 4;
    /*package*/ static final int STATE_ENABLED_AVAILABLE = 5;
    /*package*/ static final int STATE_DISABLED_AVAILABLE = 6;
    private int mState = STATE_UNINITIALIZED;

    private boolean mFeatureEnabled = false;
@@ -148,9 +148,9 @@ public class SpatializerHelper {
            .setSampleRate(48000)
            .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
            .build();
    // device array to store the routing for the default attributes and format, size 1 because
    // media is never expected to be duplicated
    private static final AudioDeviceAttributes[] ROUTING_DEVICES = new AudioDeviceAttributes[1];
    // device array to store the routing for the default attributes and format, initialized to
    // an empty list as routing hasn't been established yet
    private static ArrayList<AudioDeviceAttributes> sRoutingDevices = new ArrayList<>(0);

    //---------------------------------------------------------------
    // audio device compatibility / enabled
@@ -181,11 +181,6 @@ public class SpatializerHelper {
        SADeviceState.sHeadTrackingEnabledDefault = headTrackingEnabledByDefault;
    }

    synchronized void initForTest(boolean hasBinaural, boolean hasTransaural) {
        mBinauralSupported = hasBinaural;
        mTransauralSupported = hasTransaural;
    }

    synchronized void init(boolean effectExpected, @Nullable String settings) {
        loglogi("init effectExpected=" + effectExpected);
        if (!effectExpected) {
@@ -319,8 +314,7 @@ public class SpatializerHelper {
            return;
        }
        mState = STATE_DISABLED_UNAVAILABLE;
        mASA.getDevicesForAttributes(
                DEFAULT_ATTRIBUTES, false /* forVolume */).toArray(ROUTING_DEVICES);
        sRoutingDevices = getRoutingDevices(DEFAULT_ATTRIBUTES);
        // note at this point mSpat is still not instantiated
    }

@@ -362,34 +356,35 @@ public class SpatializerHelper {
            case STATE_DISABLED_AVAILABLE:
                break;
        }
        mASA.getDevicesForAttributes(
                DEFAULT_ATTRIBUTES, false /* forVolume */).toArray(ROUTING_DEVICES);

        sRoutingDevices = getRoutingDevices(DEFAULT_ATTRIBUTES);

        // check validity of routing information
        if (ROUTING_DEVICES[0] == null) {
            logloge("onRoutingUpdated: device is null, no Spatial Audio");
        if (sRoutingDevices.isEmpty()) {
            logloge("onRoutingUpdated: no device, no Spatial Audio");
            setDispatchAvailableState(false);
            // not changing the spatializer level as this is likely a transient state
            return;
        }
        final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0);

        // is media routed to a new device?
        if (isWireless(ROUTING_DEVICES[0].getType())) {
            addWirelessDeviceIfNew(ROUTING_DEVICES[0]);
        if (isWireless(currentDevice.getType())) {
            addWirelessDeviceIfNew(currentDevice);
        }

        // find if media device enabled / available
        final Pair<Boolean, Boolean> enabledAvailable = evaluateState(ROUTING_DEVICES[0]);
        final Pair<Boolean, Boolean> enabledAvailable = evaluateState(currentDevice);

        boolean able = false;
        if (enabledAvailable.second) {
            // available for Spatial audio, check w/ effect
            able = canBeSpatializedOnDevice(DEFAULT_ATTRIBUTES, DEFAULT_FORMAT, ROUTING_DEVICES);
            able = canBeSpatializedOnDevice(DEFAULT_ATTRIBUTES, DEFAULT_FORMAT, sRoutingDevices);
            loglogi("onRoutingUpdated: can spatialize media 5.1:" + able
                    + " on device:" + ROUTING_DEVICES[0]);
                    + " on device:" + currentDevice);
            setDispatchAvailableState(able);
        } else {
            loglogi("onRoutingUpdated: device:" + ROUTING_DEVICES[0]
            loglogi("onRoutingUpdated: device:" + currentDevice
                    + " not available for Spatial Audio");
            setDispatchAvailableState(false);
        }
@@ -397,10 +392,10 @@ public class SpatializerHelper {
        boolean enabled = able && enabledAvailable.first;
        if (enabled) {
            loglogi("Enabling Spatial Audio since enabled for media device:"
                    + ROUTING_DEVICES[0]);
                    + currentDevice);
        } else {
            loglogi("Disabling Spatial Audio since disabled for media device:"
                    + ROUTING_DEVICES[0]);
                    + currentDevice);
        }
        if (mSpat != null) {
            byte level = enabled ? (byte) Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL
@@ -733,9 +728,13 @@ public class SpatializerHelper {
    }

    private synchronized boolean canBeSpatializedOnDevice(@NonNull AudioAttributes attributes,
            @NonNull AudioFormat format, @NonNull AudioDeviceAttributes[] devices) {
        if (isDeviceCompatibleWithSpatializationModes(devices[0])) {
            return AudioSystem.canBeSpatialized(attributes, format, devices);
            @NonNull AudioFormat format, @NonNull ArrayList<AudioDeviceAttributes> devices) {
        if (devices.isEmpty()) {
            return false;
        }
        if (isDeviceCompatibleWithSpatializationModes(devices.get(0))) {
            AudioDeviceAttributes[] devArray = new AudioDeviceAttributes[devices.size()];
            return AudioSystem.canBeSpatialized(attributes, format, devices.toArray(devArray));
        }
        return false;
    }
@@ -1011,10 +1010,13 @@ public class SpatializerHelper {
                logd("canBeSpatialized false due to usage:" + attributes.getUsage());
                return false;
        }
        AudioDeviceAttributes[] devices = new AudioDeviceAttributes[1];

        // going through adapter to take advantage of routing cache
        mASA.getDevicesForAttributes(
                attributes, false /* forVolume */).toArray(devices);
        final ArrayList<AudioDeviceAttributes> devices = getRoutingDevices(attributes);
        if (devices.isEmpty()) {
            logloge("canBeSpatialized got no device for " + attributes);
            return false;
        }
        final boolean able = canBeSpatializedOnDevice(attributes, format, devices);
        logd("canBeSpatialized usage:" + attributes.getUsage()
                + " format:" + format.toLogFriendlyString() + " returning " + able);
@@ -1123,8 +1125,13 @@ public class SpatializerHelper {
        logDeviceState(deviceState, "setHeadTrackerEnabled");

        // check current routing to see if it affects the headtracking mode
        if (ROUTING_DEVICES[0] != null && ROUTING_DEVICES[0].getType() == ada.getType()
                && ROUTING_DEVICES[0].getAddress().equals(ada.getAddress())) {
        if (sRoutingDevices.isEmpty()) {
            logloge("setHeadTrackerEnabled: no device, bailing");
            return;
        }
        final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0);
        if (currentDevice.getType() == ada.getType()
                && currentDevice.getAddress().equals(ada.getAddress())) {
            setDesiredHeadTrackingMode(enabled ? mDesiredHeadTrackingModeWhenEnabled
                    : Spatializer.HEAD_TRACKING_MODE_DISABLED);
            if (enabled && !mHeadTrackerAvailable) {
@@ -1669,10 +1676,11 @@ public class SpatializerHelper {

    private int getHeadSensorHandleUpdateTracker() {
        int headHandle = -1;
        final AudioDeviceAttributes currentDevice = ROUTING_DEVICES[0];
        if (currentDevice == null) {
        if (sRoutingDevices.isEmpty()) {
            logloge("getHeadSensorHandleUpdateTracker: no device, no head tracker");
            return headHandle;
        }
        final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0);
        UUID routingDeviceUuid = mAudioService.getDeviceSensorUuid(currentDevice);
        // We limit only to Sensor.TYPE_HEAD_TRACKER here to avoid confusion
        // with gaming sensors. (Note that Sensor.TYPE_ROTATION_VECTOR
@@ -1706,6 +1714,23 @@ public class SpatializerHelper {
        return screenHandle;
    }

    /**
     * Returns routing for the given attributes
     * @param aa AudioAttributes whose routing is being queried
     * @return a non-null never-empty list of devices. If the routing query failed, the list
     *     will contain null.
     */
    private @NonNull ArrayList<AudioDeviceAttributes> getRoutingDevices(AudioAttributes aa) {
        final ArrayList<AudioDeviceAttributes> devices = mASA.getDevicesForAttributes(
                aa, false /* forVolume */);
        for (AudioDeviceAttributes ada : devices) {
            if (ada == null) {
                // invalid entry, reject this routing query by returning an empty list
                return new ArrayList<>(0);
            }
        }
        return devices;
    }

    private static void loglogi(String msg) {
        AudioService.sSpatialLogger.enqueueAndLog(msg, EventLogger.Event.ALOGI, TAG);
@@ -1722,4 +1747,13 @@ public class SpatializerHelper {
    /*package*/ void clearSADevices() {
        mSADevices.clear();
    }

    /*package*/ synchronized void forceStateForTest(int state) {
        mState = state;
    }

    /*package*/ synchronized void initForTest(boolean hasBinaural, boolean hasTransaural) {
        mBinauralSupported = hasBinaural;
        mTransauralSupported = hasTransaural;
    }
}
+60 −2
Original line number Diff line number Diff line
@@ -17,12 +17,17 @@ package com.android.server.audio;

import com.android.server.audio.SpatializerHelper.SADeviceState;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioSystem;
import android.util.Log;

@@ -36,6 +41,7 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Spy;

import java.util.ArrayList;
import java.util.List;

@MediumTest
@@ -49,14 +55,33 @@ public class SpatializerHelperTest {

    @Mock private AudioService mMockAudioService;
    @Spy private AudioSystemAdapter mSpyAudioSystem;
    @Mock private AudioSystemAdapter mMockAudioSystem;

    @Before
    public void setUp() throws Exception {
        mMockAudioService = mock(AudioService.class);
        mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
    }

        mSpatHelper = new SpatializerHelper(mMockAudioService, mSpyAudioSystem,
    /**
     * Initializes mSpatHelper, the SpatizerHelper instance under test, to use the mock or spy
     * AudioSystemAdapter
     * @param useSpyAudioSystem true to use the spy adapter, mSpyAudioSystem, or false to use
     *                          the mock adapter, mMockAudioSystem.
     */
    private void setUpSpatHelper(boolean useSpyAudioSystem) {
        final AudioSystemAdapter asAdapter;
        if (useSpyAudioSystem) {
            mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
            asAdapter = mSpyAudioSystem;
            mMockAudioSystem = null;
        } else {
            mSpyAudioSystem = null;
            mMockAudioSystem = mock(NoOpAudioSystemAdapter.class);
            asAdapter = mMockAudioSystem;
        }
        mSpatHelper = new SpatializerHelper(mMockAudioService, asAdapter,
                false /*headTrackingEnabledByDefault*/);

    }

    /**
@@ -66,6 +91,7 @@ public class SpatializerHelperTest {
     */
    @Test
    public void testSADeviceStateNullAddressCtor() throws Exception {
        setUpSpatHelper(true /*useSpyAudioSystem*/);
        try {
            SADeviceState devState = new SADeviceState(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, null);
            devState = new SADeviceState(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, null);
@@ -76,6 +102,7 @@ public class SpatializerHelperTest {
    @Test
    public void testSADeviceStateStringSerialization() throws Exception {
        Log.i(TAG, "starting testSADeviceStateStringSerialization");
        setUpSpatHelper(true /*useSpyAudioSystem*/);
        final SADeviceState devState = new SADeviceState(
                AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, "bla");
        devState.mHasHeadTracker = false;
@@ -91,6 +118,7 @@ public class SpatializerHelperTest {
    @Test
    public void testSADeviceSettings() throws Exception {
        Log.i(TAG, "starting testSADeviceSettings");
        setUpSpatHelper(true /*useSpyAudioSystem*/);
        final AudioDeviceAttributes dev1 =
                new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, "");
        final AudioDeviceAttributes dev2 =
@@ -141,4 +169,34 @@ public class SpatializerHelperTest {
        Log.i(TAG, "device settingsRestored: " + settingsRestored);
        Assert.assertEquals(settings, settingsRestored);
    }

    /**
     * Test that null devices for routing do not break canBeSpatialized
     * @throws Exception
     */
    @Test
    public void testNoRoutingCanBeSpatialized() throws Exception {
        Log.i(TAG, "Starting testNoRoutingCanBeSpatialized");
        setUpSpatHelper(false /*useSpyAudioSystem*/);
        mSpatHelper.forceStateForTest(SpatializerHelper.STATE_ENABLED_AVAILABLE);

        final ArrayList<AudioDeviceAttributes> emptyList = new ArrayList<>(0);
        final ArrayList<AudioDeviceAttributes> listWithNull = new ArrayList<>(1);
        listWithNull.add(null);
        final AudioAttributes media = new AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA).build();
        final AudioFormat spatialFormat = new AudioFormat.Builder()
                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1).build();

        when(mMockAudioSystem.getDevicesForAttributes(any(AudioAttributes.class), anyBoolean()))
                .thenReturn(emptyList);
        Assert.assertFalse("can be spatialized on empty routing",
                mSpatHelper.canBeSpatialized(media, spatialFormat));

        when(mMockAudioSystem.getDevicesForAttributes(any(AudioAttributes.class), anyBoolean()))
                .thenReturn(listWithNull);
        Assert.assertFalse("can be spatialized on null routing",
                mSpatHelper.canBeSpatialized(media, spatialFormat));
    }
}