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

Commit f08958b4 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "RESTRICT AUTOMERGE Implement Spatial Audio and Head Tracking option in...

Merge "RESTRICT AUTOMERGE Implement Spatial Audio and Head Tracking option in bluetooth settings" into tm-dev
parents 23074210 7b5f8ad8
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -14026,4 +14026,13 @@
    <!-- Text to explain an activity is a temporary placeholder [CHAR LIMIT=none] -->
    <string name="placeholder_activity" translatable="false">*This is a temporary placeholder fallback activity.</string>
    <!-- The title of the spatial audio [CHAR LIMIT=none] -->
    <string name="bluetooth_details_spatial_audio_title">Spatial audio</string>
    <!-- The summary of the spatial audio [CHAR LIMIT=none] -->
    <string name="bluetooth_details_spatial_audio_summary">Immersive audio seems like it\u0027s coming from all around you. Only works with some media.</string>
    <!-- The title of the head tracking [CHAR LIMIT=none] -->
    <string name="bluetooth_details_head_tracking_title">Make audio more realistic</string>
    <!-- The summary of the head tracking [CHAR LIMIT=none] -->
    <string name="bluetooth_details_head_tracking_summary">Shift positioning of audio so it sounds more natural.</string>
</resources>
+3 −0
Original line number Diff line number Diff line
@@ -52,6 +52,9 @@
    <PreferenceCategory
        android:key="device_companion_apps"/>

    <PreferenceCategory
        android:key="spatial_audio_group"/>

    <PreferenceCategory
        android:key="bluetooth_profiles"/>

+155 −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 com.android.settings.bluetooth;

import android.content.Context;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.Spatializer;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;

import com.android.settings.R;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.core.lifecycle.Lifecycle;

/**
 * The controller of the Spatial audio setting in the bluetooth detail settings.
 */
public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsController
        implements Preference.OnPreferenceClickListener {

    private static final String TAG = "BluetoothSpatialAudioController";
    private static final String KEY_SPATIAL_AUDIO_GROUP = "spatial_audio_group";
    private static final String KEY_SPATIAL_AUDIO = "spatial_audio";
    private static final String KEY_HEAD_TRACKING = "head_tracking";

    private final Spatializer mSpatializer;

    @VisibleForTesting
    PreferenceCategory mProfilesContainer;
    @VisibleForTesting
    AudioDeviceAttributes mAudioDevice;

    public BluetoothDetailsSpatialAudioController(
            Context context,
            PreferenceFragmentCompat fragment,
            CachedBluetoothDevice device,
            Lifecycle lifecycle) {
        super(context, fragment, device, lifecycle);
        AudioManager audioManager = context.getSystemService(AudioManager.class);
        mSpatializer = audioManager.getSpatializer();
        mAudioDevice = new AudioDeviceAttributes(
                AudioDeviceAttributes.ROLE_OUTPUT,
                AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
                mCachedDevice.getAddress());

    }

    @Override
    public boolean isAvailable() {
        return mSpatializer.isAvailableForDevice(mAudioDevice) ? true : false;
    }

    @Override
    public boolean onPreferenceClick(Preference preference) {
        SwitchPreference switchPreference = (SwitchPreference) preference;
        String key = switchPreference.getKey();
        if (TextUtils.equals(key, KEY_SPATIAL_AUDIO)) {
            if (switchPreference.isChecked()) {
                mSpatializer.addCompatibleAudioDevice(mAudioDevice);
            } else {
                mSpatializer.removeCompatibleAudioDevice(mAudioDevice);
            }
            refresh();
            return true;
        } else if (TextUtils.equals(key, KEY_HEAD_TRACKING)) {
            mSpatializer.setHeadTrackerEnabled(switchPreference.isChecked(), mAudioDevice);
            return true;
        } else {
            Log.w(TAG, "invalid key name.");
            return false;
        }
    }

    @Override
    public String getPreferenceKey() {
        return KEY_SPATIAL_AUDIO_GROUP;
    }

    @Override
    protected void init(PreferenceScreen screen) {
        mProfilesContainer = screen.findPreference(getPreferenceKey());
        mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category);
        refresh();
    }

    @Override
    protected void refresh() {
        SwitchPreference spatialAudioPref = mProfilesContainer.findPreference(KEY_SPATIAL_AUDIO);
        if (spatialAudioPref == null) {
            spatialAudioPref = createSpatialAudioPreference(mProfilesContainer.getContext());
            mProfilesContainer.addPreference(spatialAudioPref);
        }

        boolean isSpatialAudioOn = mSpatializer.getCompatibleAudioDevices().contains(mAudioDevice);
        Log.d(TAG, "refresh() isSpatialAudioOn : " + isSpatialAudioOn);
        spatialAudioPref.setChecked(isSpatialAudioOn);

        SwitchPreference headTrackingPref = mProfilesContainer.findPreference(KEY_HEAD_TRACKING);
        if (headTrackingPref == null) {
            headTrackingPref = createHeadTrackingPreference(mProfilesContainer.getContext());
            mProfilesContainer.addPreference(headTrackingPref);
        }

        boolean isHeadTrackingAvailable =
                isSpatialAudioOn && mSpatializer.hasHeadTracker(mAudioDevice);
        Log.d(TAG, "refresh() has head tracker : " + mSpatializer.hasHeadTracker(mAudioDevice));
        headTrackingPref.setVisible(isHeadTrackingAvailable);
        if (isHeadTrackingAvailable) {
            headTrackingPref.setChecked(mSpatializer.isHeadTrackerEnabled(mAudioDevice));
        }
    }

    @VisibleForTesting
    SwitchPreference createSpatialAudioPreference(Context context) {
        SwitchPreference pref = new SwitchPreference(context);
        pref.setKey(KEY_SPATIAL_AUDIO);
        pref.setTitle(context.getString(R.string.bluetooth_details_spatial_audio_title));
        pref.setSummary(context.getString(R.string.bluetooth_details_spatial_audio_summary));
        pref.setOnPreferenceClickListener(this);
        return pref;
    }

    @VisibleForTesting
    SwitchPreference createHeadTrackingPreference(Context context) {
        SwitchPreference pref = new SwitchPreference(context);
        pref.setKey(KEY_HEAD_TRACKING);
        pref.setTitle(context.getString(R.string.bluetooth_details_head_tracking_title));
        pref.setSummary(context.getString(R.string.bluetooth_details_head_tracking_summary));
        pref.setOnPreferenceClickListener(this);
        return pref;
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -187,6 +187,8 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
                    lifecycle));
            controllers.add(new BluetoothDetailsCompanionAppsController(context, this,
                    mCachedDevice, lifecycle));
            controllers.add(new BluetoothDetailsSpatialAudioController(context, this, mCachedDevice,
                    lifecycle));
            controllers.add(new BluetoothDetailsProfilesController(context, this, mManager,
                    mCachedDevice, lifecycle));
            controllers.add(new BluetoothDetailsMacAddressController(context, this, mCachedDevice,
+206 −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 com.android.settings.bluetooth;

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

import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.media.AudioDeviceAttributes;
import android.media.AudioManager;
import android.media.Spatializer;

import androidx.preference.PreferenceCategory;
import androidx.preference.SwitchPreference;

import com.android.settingslib.core.lifecycle.Lifecycle;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;

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

@RunWith(RobolectricTestRunner.class)
public class BluetoothDetailsSpatialAudioControllerTest extends BluetoothDetailsControllerTestBase {

    private static final String MAC_ADDRESS = "04:52:C7:0B:D8:3C";
    private static final String KEY_SPATIAL_AUDIO = "spatial_audio";
    private static final String KEY_HEAD_TRACKING = "head_tracking";

    @Mock
    private AudioManager mAudioManager;
    @Mock
    private Spatializer mSpatializer;
    @Mock
    private Lifecycle mSpatialAudioLifecycle;
    @Mock
    private PreferenceCategory mProfilesContainer;

    private BluetoothDetailsSpatialAudioController mController;
    private SwitchPreference mSpatialAudioPref;
    private SwitchPreference mHeadTrackingPref;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        mContext = spy(RuntimeEnvironment.application);
        when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager);
        when(mAudioManager.getSpatializer()).thenReturn(mSpatializer);
        when(mCachedDevice.getAddress()).thenReturn(MAC_ADDRESS);

        mController = new BluetoothDetailsSpatialAudioController(mContext, mFragment,
                mCachedDevice, mSpatialAudioLifecycle);
        mController.mProfilesContainer = mProfilesContainer;

        mSpatialAudioPref = mController.createSpatialAudioPreference(mContext);
        mHeadTrackingPref = mController.createHeadTrackingPreference(mContext);

        when(mProfilesContainer.findPreference(KEY_SPATIAL_AUDIO)).thenReturn(mSpatialAudioPref);
        when(mProfilesContainer.findPreference(KEY_HEAD_TRACKING)).thenReturn(mHeadTrackingPref);
    }

    @Test
    public void isAvailable_spatialAudioIsAvailable_returnsTrue() {
        when(mSpatializer.isAvailableForDevice(mController.mAudioDevice)).thenReturn(true);
        assertThat(mController.isAvailable()).isTrue();
    }

    @Test
    public void isAvailable_spatialAudioIsNotAvailable_returnsFalse() {
        when(mSpatializer.isAvailableForDevice(mController.mAudioDevice)).thenReturn(false);
        assertThat(mController.isAvailable()).isFalse();
    }

    @Test
    public void refresh_spatialAudioIsTurnedOn_checksSpatialAudioPreference() {
        List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>();
        compatibleAudioDevices.add(mController.mAudioDevice);
        when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices);

        mController.refresh();

        assertThat(mSpatialAudioPref.isChecked()).isTrue();
    }

    @Test
    public void refresh_spatialAudioIsTurnedOff_unchecksSpatialAudioPreference() {
        List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>();
        when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices);

        mController.refresh();

        assertThat(mSpatialAudioPref.isChecked()).isFalse();
    }

    @Test
    public void refresh_spatialAudioOnAndHeadTrackingIsAvailable_showsHeadTrackingPreference() {
        List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>();
        compatibleAudioDevices.add(mController.mAudioDevice);
        when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices);
        when(mSpatializer.hasHeadTracker(mController.mAudioDevice)).thenReturn(true);

        mController.refresh();

        assertThat(mHeadTrackingPref.isVisible()).isTrue();
    }

    @Test
    public void
            refresh_spatialAudioOnAndHeadTrackingIsNotAvailable_hidesHeadTrackingPreference() {
        List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>();
        compatibleAudioDevices.add(mController.mAudioDevice);
        when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices);
        when(mSpatializer.hasHeadTracker(mController.mAudioDevice)).thenReturn(false);

        mController.refresh();

        assertThat(mHeadTrackingPref.isVisible()).isFalse();
    }

    @Test
    public void refresh_spatialAudioOff_hidesHeadTrackingPreference() {
        List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>();
        when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices);

        mController.refresh();

        assertThat(mHeadTrackingPref.isVisible()).isFalse();
    }

    @Test
    public void refresh_headTrackingIsTurnedOn_checksHeadTrackingPreference() {
        List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>();
        compatibleAudioDevices.add(mController.mAudioDevice);
        when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices);
        when(mSpatializer.hasHeadTracker(mController.mAudioDevice)).thenReturn(true);
        when(mSpatializer.isHeadTrackerEnabled(mController.mAudioDevice)).thenReturn(true);

        mController.refresh();

        assertThat(mHeadTrackingPref.isChecked()).isTrue();
    }

    @Test
    public void refresh_headTrackingIsTurnedOff_unchecksHeadTrackingPreference() {
        List<AudioDeviceAttributes> compatibleAudioDevices = new ArrayList<>();
        compatibleAudioDevices.add(mController.mAudioDevice);
        when(mSpatializer.getCompatibleAudioDevices()).thenReturn(compatibleAudioDevices);
        when(mSpatializer.hasHeadTracker(mController.mAudioDevice)).thenReturn(true);
        when(mSpatializer.isHeadTrackerEnabled(mController.mAudioDevice)).thenReturn(false);

        mController.refresh();

        assertThat(mHeadTrackingPref.isChecked()).isFalse();
    }

    @Test
    public void turnedOnSpatialAudio_invokesAddCompatibleAudioDevice() {
        mSpatialAudioPref.setChecked(true);
        mController.onPreferenceClick(mSpatialAudioPref);
        verify(mSpatializer).addCompatibleAudioDevice(mController.mAudioDevice);
    }

    @Test
    public void turnedOffSpatialAudio_invokesRemoveCompatibleAudioDevice() {
        mSpatialAudioPref.setChecked(false);
        mController.onPreferenceClick(mSpatialAudioPref);
        verify(mSpatializer).removeCompatibleAudioDevice(mController.mAudioDevice);
    }

    @Test
    public void turnedOnHeadTracking_invokesSetHeadTrackerEnabled_setsTrue() {
        mHeadTrackingPref.setChecked(true);
        mController.onPreferenceClick(mHeadTrackingPref);
        verify(mSpatializer).setHeadTrackerEnabled(true, mController.mAudioDevice);
    }

    @Test
    public void turnedOffHeadTracking_invokesSetHeadTrackerEnabled_setsFalse() {
        mHeadTrackingPref.setChecked(false);
        mController.onPreferenceClick(mHeadTrackingPref);
        verify(mSpatializer).setHeadTrackerEnabled(false, mController.mAudioDevice);
    }
}