Implement Spatial audio toggle domain layer
BUG: 343317785 Test: atest SpatialAudioInteractorTest Flag: com.android.settings.flags.enable_bluetooth_device_details_polish Change-Id: Ic73e56a1ca41f9fa58d5219666478a7edc55059d
This commit is contained in:
26
res/drawable/ic_head_tracking.xml
Normal file
26
res/drawable/ic_head_tracking.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
~ Copyright (C) 2024 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.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="960"
|
||||
android:viewportWidth="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,520Q414,520 367,473Q320,426 320,360Q320,294 367,247Q414,200 480,200Q546,200 593,247Q640,294 640,360Q640,426 593,473Q546,520 480,520ZM160,840L160,728Q160,695 177,666Q194,637 224,622Q275,596 339,578Q403,560 480,560Q557,560 621,578Q685,596 736,622Q766,637 783,666Q800,695 800,728L800,840L160,840ZM240,760L720,760L720,728Q720,717 714.5,708Q709,699 700,694Q664,676 607.5,658Q551,640 480,640Q409,640 352.5,658Q296,676 260,694Q251,699 245.5,708Q240,717 240,728L240,760ZM480,440Q513,440 536.5,416.5Q560,393 560,360Q560,327 536.5,303.5Q513,280 480,280Q447,280 423.5,303.5Q400,327 400,360Q400,393 423.5,416.5Q447,440 480,440ZM39,200L39,120Q56,120 70,113.5Q84,107 95,96Q106,85 112,71Q118,57 118,40L199,40Q199,73 186.5,102Q174,131 152,153Q130,175 101,187.5Q72,200 39,200ZM39,361L39,281Q90,281 133.5,262Q177,243 209,210Q241,177 260,133.5Q279,90 279,40L360,40Q360,106 335,164.5Q310,223 266,267Q222,311 164,336Q106,361 39,361ZM920,361Q854,361 795.5,336Q737,311 693,267Q649,223 624,164.5Q599,106 599,40L679,40Q679,90 698,133.5Q717,177 750,210Q783,243 826.5,262Q870,281 920,281L920,361ZM920,200Q887,200 858,187.5Q829,175 807,153Q785,131 772.5,102Q760,73 760,40L840,40Q840,57 846.5,71Q853,85 864,96Q875,107 889,113.5Q903,120 920,120L920,200ZM480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360ZM480,760L480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760L480,760L480,760Z" />
|
||||
</vector>
|
26
res/drawable/ic_spatial_audio.xml
Normal file
26
res/drawable/ic_spatial_audio.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
~ Copyright (C) 2024 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.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="960"
|
||||
android:viewportWidth="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M920,401Q848,401 782,373.5Q716,346 665,295Q614,244 586.5,178Q559,112 559,40L639,40Q639,97 660,148Q681,199 721,239Q761,279 812,300.5Q863,322 920,322L920,401ZM920,242Q879,242 842.5,227Q806,212 777,183Q748,154 733,117.5Q718,81 718,40L797,40Q797,65 806.5,87.5Q816,110 833,127Q850,144 872.5,153Q895,162 920,162L920,242ZM400,520Q334,520 287,473Q240,426 240,360Q240,294 287,247Q334,200 400,200Q466,200 513,247Q560,294 560,360Q560,426 513,473Q466,520 400,520ZM80,840L80,728Q80,695 97,666Q114,637 144,622Q195,596 259,578Q323,560 400,560Q477,560 541,578Q605,596 656,622Q686,637 703,666Q720,695 720,728L720,840L80,840ZM160,760L640,760L640,728Q640,717 634.5,708Q629,699 620,694Q584,676 527.5,658Q471,640 400,640Q329,640 272.5,658Q216,676 180,694Q171,699 165.5,708Q160,717 160,728L160,760ZM400,440Q433,440 456.5,416.5Q480,393 480,360Q480,327 456.5,303.5Q433,280 400,280Q367,280 343.5,303.5Q320,327 320,360Q320,393 343.5,416.5Q367,440 400,440ZM400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360ZM400,760L400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760L400,760L400,760Z" />
|
||||
</vector>
|
26
res/drawable/ic_spatial_audio_off.xml
Normal file
26
res/drawable/ic_spatial_audio_off.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
~ Copyright (C) 2024 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.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="960"
|
||||
android:viewportWidth="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M750,550L806,494Q766,454 743.5,402.5Q721,351 721,294Q721,237 743.5,186Q766,135 806,95L750,37Q699,88 670,155Q641,222 641,294Q641,366 670,432.5Q699,499 750,550ZM862,436L918,380Q901,363 891,341Q881,319 881,294Q881,269 891,247Q901,225 918,208L862,151Q833,180 817,216Q801,252 801,293Q801,334 817,371Q833,408 862,436ZM400,520Q334,520 287,473Q240,426 240,360Q240,294 287,247Q334,200 400,200Q466,200 513,247Q560,294 560,360Q560,426 513,473Q466,520 400,520ZM80,840L80,728Q80,695 97,666Q114,637 144,622Q195,596 259,578Q323,560 400,560Q477,560 541,578Q605,596 656,622Q686,637 703,666Q720,695 720,728L720,840L80,840ZM160,760L640,760L640,728Q640,717 634.5,708Q629,699 620,694Q584,676 527.5,658Q471,640 400,640Q329,640 272.5,658Q216,676 180,694Q171,699 165.5,708Q160,717 160,728L160,760ZM400,440Q433,440 456.5,416.5Q480,393 480,360Q480,327 456.5,303.5Q433,280 400,280Q367,280 343.5,303.5Q320,327 320,360Q320,393 343.5,416.5Q367,440 400,440ZM400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360ZM400,760L400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760L400,760L400,760Z" />
|
||||
</vector>
|
@@ -7946,6 +7946,18 @@
|
||||
<!-- Sound: Footer hyperlink text to launch the Connected devices settings page. [CHAR LIMIT=NONE]-->
|
||||
<string name="spatial_audio_footer_learn_more_text">Connected devices settings</string>
|
||||
|
||||
<!-- Bluetooth device details: spatial audio multi-toggle title. [CHAR LIMIT=20]-->
|
||||
<string name="spatial_audio_multi_toggle_title">Spatial Audio</string>
|
||||
|
||||
<!-- Bluetooth device details: spatial audio is off. [CHAR LIMIT=20]-->
|
||||
<string name="spatial_audio_multi_toggle_off">Off</string>
|
||||
|
||||
<!-- Bluetooth device details: spatial audio is on. [CHAR LIMIT=20]-->
|
||||
<string name="spatial_audio_multi_toggle_on">Off</string>
|
||||
|
||||
<!-- Bluetooth device details: head tracking is on. [CHAR LIMIT=20]-->
|
||||
<string name="spatial_audio_multi_toggle_head_tracking_on">Off</string>
|
||||
|
||||
<!-- Zen Modes: Summary for the Do not Disturb option that describes how many automatic rules (schedules) are enabled [CHAR LIMIT=NONE]-->
|
||||
<string name="zen_mode_settings_schedules_summary">
|
||||
{count, plural,
|
||||
|
@@ -39,8 +39,8 @@ import androidx.preference.TwoStatePreference;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.overlay.FeatureFactory;
|
||||
import com.android.settingslib.bluetooth.BluetoothUtils;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothProfile;
|
||||
import com.android.settingslib.core.lifecycle.Lifecycle;
|
||||
import com.android.settingslib.flags.Flags;
|
||||
import com.android.settingslib.utils.ThreadUtils;
|
||||
@@ -299,57 +299,14 @@ public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsCont
|
||||
+ " profiles: "
|
||||
+ mCachedDevice.getProfiles());
|
||||
|
||||
AudioDeviceAttributes saDevice = null;
|
||||
for (LocalBluetoothProfile profile : mCachedDevice.getProfiles()) {
|
||||
// pick first enabled profile that is compatible with spatial audio
|
||||
if (SA_PROFILES.contains(profile.getProfileId())
|
||||
&& profile.isEnabled(mCachedDevice.getDevice())) {
|
||||
switch (profile.getProfileId()) {
|
||||
case BluetoothProfile.A2DP:
|
||||
saDevice =
|
||||
new AudioDeviceAttributes(
|
||||
AudioDeviceAttributes.ROLE_OUTPUT,
|
||||
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
|
||||
mCachedDevice.getAddress());
|
||||
break;
|
||||
case BluetoothProfile.LE_AUDIO:
|
||||
if (mAudioManager.getBluetoothAudioDeviceCategory(
|
||||
mCachedDevice.getAddress())
|
||||
== AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER) {
|
||||
saDevice =
|
||||
new AudioDeviceAttributes(
|
||||
AudioDeviceAttributes.ROLE_OUTPUT,
|
||||
AudioDeviceInfo.TYPE_BLE_SPEAKER,
|
||||
mCachedDevice.getAddress());
|
||||
} else {
|
||||
saDevice =
|
||||
new AudioDeviceAttributes(
|
||||
AudioDeviceAttributes.ROLE_OUTPUT,
|
||||
AudioDeviceInfo.TYPE_BLE_HEADSET,
|
||||
mCachedDevice.getAddress());
|
||||
}
|
||||
|
||||
break;
|
||||
case BluetoothProfile.HEARING_AID:
|
||||
saDevice =
|
||||
new AudioDeviceAttributes(
|
||||
AudioDeviceAttributes.ROLE_OUTPUT,
|
||||
AudioDeviceInfo.TYPE_HEARING_AID,
|
||||
mCachedDevice.getAddress());
|
||||
break;
|
||||
default:
|
||||
Log.i(
|
||||
TAG,
|
||||
"unrecognized profile for spatial audio: "
|
||||
+ profile.getProfileId());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
mAudioDevice = null;
|
||||
AudioDeviceAttributes saDevice =
|
||||
BluetoothUtils.getAudioDeviceAttributesForSpatialAudio(
|
||||
mCachedDevice,
|
||||
mAudioManager.getBluetoothAudioDeviceCategory(mCachedDevice.getAddress()));
|
||||
if (saDevice != null && mSpatializer.isAvailableForDevice(saDevice)) {
|
||||
mAudioDevice = saDevice;
|
||||
} else {
|
||||
mAudioDevice = null;
|
||||
}
|
||||
|
||||
Log.d(
|
||||
|
@@ -20,6 +20,7 @@ import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.Spatializer;
|
||||
import android.net.Uri;
|
||||
|
||||
@@ -28,6 +29,7 @@ import androidx.lifecycle.LifecycleCoroutineScope;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.SettingsPreferenceFragment;
|
||||
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor;
|
||||
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository;
|
||||
@@ -98,6 +100,13 @@ public interface BluetoothFeatureProvider {
|
||||
@NonNull BluetoothAdapter bluetoothAdapter,
|
||||
@NonNull LifecycleCoroutineScope scope);
|
||||
|
||||
/** Gets spatial audio interactor. */
|
||||
@NonNull
|
||||
SpatialAudioInteractor getSpatialAudioInteractor(
|
||||
@NonNull Context context,
|
||||
@NonNull AudioManager audioManager,
|
||||
@NonNull LifecycleCoroutineScope scope);
|
||||
|
||||
/** Gets device details fragment layout formatter. */
|
||||
@NonNull
|
||||
DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter(
|
||||
|
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.android.settings.bluetooth.utils.DeviceSettingUtilsKt.createDeviceSettingRepository;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.Spatializer;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LifecycleCoroutineScope;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.SettingsPreferenceFragment;
|
||||
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
|
||||
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatterImpl;
|
||||
import com.android.settingslib.bluetooth.BluetoothUtils;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Impl of {@link BluetoothFeatureProvider}
|
||||
*/
|
||||
public class BluetoothFeatureProviderImpl implements BluetoothFeatureProvider {
|
||||
|
||||
@Override
|
||||
public Uri getBluetoothDeviceSettingsUri(BluetoothDevice bluetoothDevice) {
|
||||
final byte[] uriByte = bluetoothDevice.getMetadata(
|
||||
BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI);
|
||||
return uriByte == null ? null : Uri.parse(new String(uriByte));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBluetoothDeviceControlUri(BluetoothDevice bluetoothDevice) {
|
||||
return BluetoothUtils.getControlUriMetaData(bluetoothDevice);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ComponentName> getRelatedTools() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spatializer getSpatializer(Context context) {
|
||||
AudioManager audioManager = context.getSystemService(AudioManager.class);
|
||||
return audioManager.getSpatializer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Preference> getBluetoothExtraOptions(Context context,
|
||||
CachedBluetoothDevice device) {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getInvisibleProfilePreferenceKeys(
|
||||
Context context, BluetoothDevice bluetoothDevice) {
|
||||
return ImmutableSet.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public DeviceSettingRepository getDeviceSettingRepository(
|
||||
@NonNull Context context,
|
||||
@NonNull BluetoothAdapter bluetoothAdapter,
|
||||
@NonNull LifecycleCoroutineScope scope) {
|
||||
return createDeviceSettingRepository(context, bluetoothAdapter, scope);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter(
|
||||
@NonNull Context context,
|
||||
@NonNull SettingsPreferenceFragment fragment,
|
||||
@NonNull BluetoothAdapter bluetoothAdapter,
|
||||
@NonNull CachedBluetoothDevice cachedDevice) {
|
||||
return new DeviceDetailsFragmentFormatterImpl(
|
||||
context, fragment, bluetoothAdapter, cachedDevice);
|
||||
}
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import android.media.Spatializer
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.preference.Preference
|
||||
import com.android.settings.SettingsPreferenceFragment
|
||||
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
|
||||
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractorImpl
|
||||
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter
|
||||
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatterImpl
|
||||
import com.android.settingslib.bluetooth.BluetoothUtils
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice
|
||||
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
|
||||
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepositoryImpl
|
||||
import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl
|
||||
import com.android.settingslib.media.domain.interactor.SpatializerInteractor
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.collect.ImmutableSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/** Impl of [BluetoothFeatureProvider] */
|
||||
open class BluetoothFeatureProviderImpl : BluetoothFeatureProvider {
|
||||
override fun getBluetoothDeviceSettingsUri(bluetoothDevice: BluetoothDevice): Uri? {
|
||||
val uriByte = bluetoothDevice.getMetadata(BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI)
|
||||
return uriByte?.let { Uri.parse(String(it)) }
|
||||
}
|
||||
|
||||
override fun getBluetoothDeviceControlUri(bluetoothDevice: BluetoothDevice): String? {
|
||||
return BluetoothUtils.getControlUriMetaData(bluetoothDevice)
|
||||
}
|
||||
|
||||
override fun getRelatedTools(): List<ComponentName>? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getSpatializer(context: Context): Spatializer? {
|
||||
val audioManager = context.getSystemService(AudioManager::class.java)
|
||||
return audioManager.spatializer
|
||||
}
|
||||
|
||||
override fun getBluetoothExtraOptions(
|
||||
context: Context,
|
||||
device: CachedBluetoothDevice
|
||||
): List<Preference>? {
|
||||
return ImmutableList.of<Preference>()
|
||||
}
|
||||
|
||||
override fun getInvisibleProfilePreferenceKeys(
|
||||
context: Context,
|
||||
bluetoothDevice: BluetoothDevice
|
||||
): Set<String> {
|
||||
return ImmutableSet.of()
|
||||
}
|
||||
|
||||
override fun getDeviceSettingRepository(
|
||||
context: Context,
|
||||
bluetoothAdapter: BluetoothAdapter,
|
||||
scope: LifecycleCoroutineScope
|
||||
): DeviceSettingRepository =
|
||||
DeviceSettingRepositoryImpl(context, bluetoothAdapter, scope, Dispatchers.IO)
|
||||
|
||||
override fun getSpatialAudioInteractor(
|
||||
context: Context,
|
||||
audioManager: AudioManager,
|
||||
scope: LifecycleCoroutineScope
|
||||
): SpatialAudioInteractor {
|
||||
return SpatialAudioInteractorImpl(
|
||||
context, audioManager,
|
||||
SpatializerInteractor(
|
||||
SpatializerRepositoryImpl(
|
||||
audioManager.spatializer,
|
||||
Dispatchers.IO
|
||||
)
|
||||
), scope, Dispatchers.IO)
|
||||
}
|
||||
|
||||
override fun getDeviceDetailsFragmentFormatter(
|
||||
context: Context,
|
||||
fragment: SettingsPreferenceFragment,
|
||||
bluetoothAdapter: BluetoothAdapter,
|
||||
cachedDevice: CachedBluetoothDevice
|
||||
): DeviceDetailsFragmentFormatter {
|
||||
return DeviceDetailsFragmentFormatterImpl(context, fragment, bluetoothAdapter, cachedDevice)
|
||||
}
|
||||
}
|
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.domain.interactor
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import android.util.Log
|
||||
import com.android.settings.R
|
||||
import com.android.settingslib.bluetooth.BluetoothUtils
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice
|
||||
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
|
||||
import com.android.settingslib.media.domain.interactor.SpatializerInteractor
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** Provides device setting for spatial audio. */
|
||||
interface SpatialAudioInteractor {
|
||||
/** Gets device setting for spatial audio */
|
||||
fun getDeviceSetting(
|
||||
cachedDevice: CachedBluetoothDevice,
|
||||
): Flow<DeviceSettingModel?>
|
||||
}
|
||||
|
||||
class SpatialAudioInteractorImpl(
|
||||
private val context: Context,
|
||||
private val audioManager: AudioManager,
|
||||
private val spatializerInteractor: SpatializerInteractor,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val backgroundCoroutineContext: CoroutineContext,
|
||||
) : SpatialAudioInteractor {
|
||||
private val spatialAudioOffToggle =
|
||||
ToggleModel(
|
||||
context.getString(R.string.spatial_audio_multi_toggle_off),
|
||||
DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio_off))
|
||||
private val spatialAudioOnToggle =
|
||||
ToggleModel(
|
||||
context.getString(R.string.spatial_audio_multi_toggle_on),
|
||||
DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio))
|
||||
private val headTrackingOnToggle =
|
||||
ToggleModel(
|
||||
context.getString(R.string.spatial_audio_multi_toggle_head_tracking_on),
|
||||
DeviceSettingIcon.ResourceIcon(R.drawable.ic_head_tracking))
|
||||
private val changes = MutableSharedFlow<Unit>()
|
||||
|
||||
override fun getDeviceSetting(
|
||||
cachedDevice: CachedBluetoothDevice,
|
||||
): Flow<DeviceSettingModel?> =
|
||||
changes
|
||||
.onStart { emit(Unit) }
|
||||
.map { getSpatialAudioDeviceSettingModel(cachedDevice) }
|
||||
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = null)
|
||||
|
||||
private suspend fun getSpatialAudioDeviceSettingModel(
|
||||
cachedDevice: CachedBluetoothDevice,
|
||||
): DeviceSettingModel? {
|
||||
// TODO(b/343317785): use audio repository instead of calling AudioManager directly.
|
||||
Log.i(TAG, "CachedDevice: $cachedDevice profiles: ${cachedDevice.profiles}")
|
||||
val attributes =
|
||||
BluetoothUtils.getAudioDeviceAttributesForSpatialAudio(
|
||||
cachedDevice, audioManager.getBluetoothAudioDeviceCategory(cachedDevice.address))
|
||||
?: run {
|
||||
Log.i(TAG, "No audio profiles in cachedDevice: ${cachedDevice.address}.")
|
||||
return null
|
||||
}
|
||||
|
||||
Log.i(TAG, "Audio device attributes for ${cachedDevice.address}: $attributes.")
|
||||
val spatialAudioAvailable = spatializerInteractor.isSpatialAudioAvailable(attributes)
|
||||
if (!spatialAudioAvailable) {
|
||||
Log.i(TAG, "Spatial audio is not available for ${cachedDevice.address}")
|
||||
return null
|
||||
}
|
||||
val headTrackingAvailable =
|
||||
spatialAudioAvailable && spatializerInteractor.isHeadTrackingAvailable(attributes)
|
||||
val toggles =
|
||||
if (headTrackingAvailable) {
|
||||
listOf(spatialAudioOffToggle, spatialAudioOnToggle, headTrackingOnToggle)
|
||||
} else {
|
||||
listOf(spatialAudioOffToggle, spatialAudioOnToggle)
|
||||
}
|
||||
val spatialAudioEnabled = spatializerInteractor.isSpatialAudioEnabled(attributes)
|
||||
val headTrackingEnabled =
|
||||
spatialAudioEnabled && spatializerInteractor.isHeadTrackingEnabled(attributes)
|
||||
|
||||
val activeIndex =
|
||||
when {
|
||||
headTrackingEnabled -> INDEX_HEAD_TRACKING_ENABLED
|
||||
spatialAudioEnabled -> INDEX_SPATIAL_AUDIO_ON
|
||||
else -> INDEX_SPATIAL_AUDIO_OFF
|
||||
}
|
||||
Log.i(
|
||||
TAG,
|
||||
"Head tracking available: $headTrackingAvailable, " +
|
||||
"spatial audio enabled: $spatialAudioEnabled, " +
|
||||
"head tracking enabled: $headTrackingEnabled")
|
||||
return DeviceSettingModel.MultiTogglePreference(
|
||||
cachedDevice = cachedDevice,
|
||||
id = DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE,
|
||||
title = context.getString(R.string.spatial_audio_multi_toggle_title),
|
||||
toggles = toggles,
|
||||
isActive = spatialAudioEnabled,
|
||||
state = DeviceSettingStateModel.MultiTogglePreferenceState(activeIndex),
|
||||
isAllowedChangingState = true,
|
||||
updateState = { newState ->
|
||||
coroutineScope.launch(backgroundCoroutineContext) {
|
||||
Log.i(TAG, "Update spatial audio state: $newState")
|
||||
when (newState.selectedIndex) {
|
||||
INDEX_SPATIAL_AUDIO_OFF -> {
|
||||
spatializerInteractor.setSpatialAudioEnabled(attributes, false)
|
||||
}
|
||||
INDEX_SPATIAL_AUDIO_ON -> {
|
||||
spatializerInteractor.setSpatialAudioEnabled(attributes, true)
|
||||
spatializerInteractor.setHeadTrackingEnabled(attributes, false)
|
||||
}
|
||||
INDEX_HEAD_TRACKING_ENABLED -> {
|
||||
spatializerInteractor.setSpatialAudioEnabled(attributes, true)
|
||||
spatializerInteractor.setHeadTrackingEnabled(attributes, true)
|
||||
}
|
||||
}
|
||||
changes.emit(Unit)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SpatialAudioInteractorImpl"
|
||||
private const val INDEX_SPATIAL_AUDIO_OFF = 0
|
||||
private const val INDEX_SPATIAL_AUDIO_ON = 1
|
||||
private const val INDEX_HEAD_TRACKING_ENABLED = 2
|
||||
}
|
||||
}
|
48
src/com/android/settings/bluetooth/ui/composable/Icon.kt
Normal file
48
src/com/android/settings/bluetooth/ui/composable/Icon.kt
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.ui.composable
|
||||
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
|
||||
|
||||
@Composable
|
||||
fun Icon(
|
||||
icon: DeviceSettingIcon,
|
||||
modifier: Modifier = Modifier,
|
||||
tint: Color = LocalContentColor.current,
|
||||
) {
|
||||
when (icon) {
|
||||
is DeviceSettingIcon.BitmapIcon ->
|
||||
androidx.compose.material3.Icon(
|
||||
icon.bitmap.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
tint = LocalContentColor.current)
|
||||
is DeviceSettingIcon.ResourceIcon ->
|
||||
androidx.compose.material3.Icon(
|
||||
painterResource(icon.resId),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
tint = tint)
|
||||
else -> {}
|
||||
}
|
||||
}
|
@@ -51,7 +51,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.boundsInParent
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -67,6 +66,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.android.settings.R
|
||||
import com.android.settings.bluetooth.ui.composable.Icon as DeviceSettingComposeIcon
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
|
||||
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
||||
@@ -97,8 +97,7 @@ fun MultiTogglePreferenceGroup(
|
||||
Surface(
|
||||
modifier = Modifier.height(64.dp),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
color = MaterialTheme.colorScheme.surface
|
||||
) {
|
||||
color = MaterialTheme.colorScheme.surface) {
|
||||
Button(
|
||||
modifier =
|
||||
Modifier.fillMaxSize().padding(8.dp).semantics {
|
||||
@@ -114,16 +113,11 @@ fun MultiTogglePreferenceGroup(
|
||||
onClick = { settingIdForPopUp = preferenceModel.id },
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = getButtonColors(preferenceModel.isActive),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Icon(
|
||||
contentPadding = PaddingValues(0.dp)) {
|
||||
DeviceSettingComposeIcon(
|
||||
preferenceModel.toggles[preferenceModel.state.selectedIndex]
|
||||
.icon
|
||||
.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = LocalContentColor.current
|
||||
)
|
||||
.icon,
|
||||
modifier = Modifier.size(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,8 +167,7 @@ private fun dialog(
|
||||
Icon(
|
||||
painterResource(id = R.drawable.ic_close),
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.inverseSurface
|
||||
)
|
||||
tint = MaterialTheme.colorScheme.inverseSurface)
|
||||
}
|
||||
Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 20.dp)) {
|
||||
dialogContent(multiTogglePreference)
|
||||
@@ -182,8 +175,7 @@ private fun dialog(
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -208,9 +200,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP
|
||||
Modifier.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surface,
|
||||
shape = RoundedCornerShape(28.dp)
|
||||
),
|
||||
MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(28.dp)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
@@ -224,9 +214,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP
|
||||
.width(selectedRect!!.width.toDp())
|
||||
.background(
|
||||
MaterialTheme.colorScheme.tertiaryContainer,
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
)
|
||||
)
|
||||
shape = RoundedCornerShape(20.dp)))
|
||||
}
|
||||
}
|
||||
Row {
|
||||
@@ -238,9 +226,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP
|
||||
.padding(horizontal = 8.dp)
|
||||
.height(48.dp)
|
||||
.background(
|
||||
Color.Transparent,
|
||||
shape = RoundedCornerShape(28.dp)
|
||||
)
|
||||
Color.Transparent, shape = RoundedCornerShape(28.dp))
|
||||
.onGloballyPositioned { layoutCoordinates ->
|
||||
if (selected) {
|
||||
selectedRect = layoutCoordinates.boundsInParent()
|
||||
@@ -252,22 +238,16 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP
|
||||
Button(
|
||||
onClick = {
|
||||
multiTogglePreference.updateState(
|
||||
DeviceSettingStateModel.MultiTogglePreferenceState(idx)
|
||||
)
|
||||
DeviceSettingStateModel.MultiTogglePreferenceState(idx))
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = LocalContentColor.current
|
||||
),
|
||||
contentColor = LocalContentColor.current),
|
||||
) {
|
||||
Icon(
|
||||
bitmap = toggle.icon.asImageBitmap(),
|
||||
null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = LocalContentColor.current
|
||||
)
|
||||
DeviceSettingComposeIcon(
|
||||
toggle.icon, modifier = Modifier.size(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,8 +265,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP
|
||||
text = toggle.label,
|
||||
fontSize = 12.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 8.dp)
|
||||
)
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -18,20 +18,19 @@ package com.android.settings.bluetooth.ui.view
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import com.android.settings.SettingsPreferenceFragment
|
||||
import com.android.settings.bluetooth.ui.composable.Icon
|
||||
import com.android.settings.bluetooth.ui.composable.MultiTogglePreferenceGroup
|
||||
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
|
||||
import com.android.settings.bluetooth.ui.viewmodel.BluetoothDeviceDetailsViewModel
|
||||
@@ -42,7 +41,6 @@ import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSetti
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
|
||||
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
||||
import com.android.settingslib.spa.widget.preference.Preference as SpaPreference
|
||||
import com.android.settingslib.spa.widget.preference.PreferenceModel
|
||||
import com.android.settingslib.spa.widget.preference.SwitchPreference
|
||||
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
|
||||
@@ -52,6 +50,8 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import com.android.settingslib.spa.widget.preference.Preference as SpaPreference
|
||||
|
||||
|
||||
/** Handles device details fragment layout according to config. */
|
||||
interface DeviceDetailsFragmentFormatter {
|
||||
@@ -72,19 +72,24 @@ class DeviceDetailsFragmentFormatterImpl(
|
||||
private val repository =
|
||||
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
|
||||
context, bluetoothAdapter, fragment.lifecycleScope)
|
||||
private val spatialAudioInteractor =
|
||||
featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor(
|
||||
context, context.getSystemService(AudioManager::class.java), fragment.lifecycleScope)
|
||||
private val viewModel: BluetoothDeviceDetailsViewModel =
|
||||
ViewModelProvider(
|
||||
fragment,
|
||||
BluetoothDeviceDetailsViewModel.Factory(
|
||||
repository,
|
||||
spatialAudioInteractor,
|
||||
cachedDevice,
|
||||
))
|
||||
.get(BluetoothDeviceDetailsViewModel::class.java)
|
||||
|
||||
override fun getVisiblePreferenceKeysForMainPage(): List<String>? = runBlocking {
|
||||
viewModel.getItems()?.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem>()?.map {
|
||||
it.preferenceKey
|
||||
}
|
||||
viewModel
|
||||
.getItems()
|
||||
?.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem>()
|
||||
?.mapNotNull { it.preferenceKey }
|
||||
}
|
||||
|
||||
/** Updates bluetooth device details fragment layout. */
|
||||
@@ -208,12 +213,8 @@ class DeviceDetailsFragmentFormatterImpl(
|
||||
|
||||
@Composable
|
||||
private fun deviceSettingIcon(model: DeviceSettingModel.ActionSwitchPreference) {
|
||||
model.icon?.let { bitmap ->
|
||||
Icon(
|
||||
bitmap.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SettingsDimension.itemIconSize),
|
||||
tint = LocalContentColor.current)
|
||||
model.icon?.let { icon ->
|
||||
Icon(icon, modifier = Modifier.size(SettingsDimension.itemIconSize))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -19,6 +19,7 @@ package com.android.settings.bluetooth.ui.viewmodel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
|
||||
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
|
||||
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice
|
||||
@@ -29,6 +30,7 @@ import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSetti
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
@@ -37,6 +39,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class BluetoothDeviceDetailsViewModel(
|
||||
private val deviceSettingRepository: DeviceSettingRepository,
|
||||
private val spatialAudioInteractor: SpatialAudioInteractor,
|
||||
private val cachedDevice: CachedBluetoothDevice,
|
||||
) : ViewModel() {
|
||||
private val items =
|
||||
@@ -46,8 +49,16 @@ class BluetoothDeviceDetailsViewModel(
|
||||
|
||||
suspend fun getItems(): List<DeviceSettingConfigItemModel>? = items.await()?.mainItems
|
||||
|
||||
fun getDeviceSetting(cachedDevice: CachedBluetoothDevice, @DeviceSettingId settingId: Int) =
|
||||
deviceSettingRepository.getDeviceSetting(cachedDevice, settingId)
|
||||
fun getDeviceSetting(
|
||||
cachedDevice: CachedBluetoothDevice,
|
||||
@DeviceSettingId settingId: Int
|
||||
): Flow<DeviceSettingModel?> {
|
||||
return when (settingId) {
|
||||
DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE ->
|
||||
spatialAudioInteractor.getDeviceSetting(cachedDevice)
|
||||
else -> deviceSettingRepository.getDeviceSetting(cachedDevice, settingId)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getLayout(): DeviceSettingLayout? {
|
||||
val configItems = getItems() ?: return null
|
||||
@@ -93,11 +104,14 @@ class BluetoothDeviceDetailsViewModel(
|
||||
|
||||
class Factory(
|
||||
private val deviceSettingRepository: DeviceSettingRepository,
|
||||
private val spatialAudioInteractor: SpatialAudioInteractor,
|
||||
private val cachedDevice: CachedBluetoothDevice,
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return BluetoothDeviceDetailsViewModel(deviceSettingRepository, cachedDevice) as T
|
||||
return BluetoothDeviceDetailsViewModel(
|
||||
deviceSettingRepository, spatialAudioInteractor, cachedDevice)
|
||||
as T
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.utils
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepositoryImpl
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
fun createDeviceSettingRepository(
|
||||
context: Context,
|
||||
bluetoothAdapter: BluetoothAdapter,
|
||||
coroutineScope: LifecycleCoroutineScope
|
||||
) = DeviceSettingRepositoryImpl(context, bluetoothAdapter, coroutineScope, Dispatchers.IO)
|
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.domain.interactor
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.content.Context
|
||||
import android.media.AudioDeviceAttributes
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.media.AudioManager
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice
|
||||
import com.android.settingslib.bluetooth.LeAudioProfile
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
|
||||
import com.android.settingslib.media.data.repository.SpatializerRepository
|
||||
import com.android.settingslib.media.domain.interactor.SpatializerInteractor
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.spy
|
||||
import org.mockito.Mockito.times
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyNoInteractions
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
import org.mockito.junit.MockitoRule
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class SpatialAudioInteractorTest {
|
||||
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
|
||||
|
||||
@Mock private lateinit var audioManager: AudioManager
|
||||
@Mock private lateinit var cachedDevice: CachedBluetoothDevice
|
||||
@Mock private lateinit var bluetoothDevice: BluetoothDevice
|
||||
@Mock private lateinit var spatializerRepository: SpatializerRepository
|
||||
@Mock private lateinit var leAudioProfile: LeAudioProfile
|
||||
|
||||
private lateinit var underTest: SpatialAudioInteractor
|
||||
private val testScope = TestScope()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val context = spy(ApplicationProvider.getApplicationContext<Context>())
|
||||
`when`(cachedDevice.device).thenReturn(bluetoothDevice)
|
||||
`when`(cachedDevice.address).thenReturn(BLUETOOTH_ADDRESS)
|
||||
`when`(leAudioProfile.profileId).thenReturn(BluetoothProfile.LE_AUDIO)
|
||||
underTest =
|
||||
SpatialAudioInteractorImpl(
|
||||
context,
|
||||
audioManager,
|
||||
SpatializerInteractor(spatializerRepository),
|
||||
testScope.backgroundScope,
|
||||
testScope.testScheduler)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getDeviceSetting_noAudioProfile_returnNull() {
|
||||
testScope.runTest {
|
||||
val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
|
||||
|
||||
assertThat(setting).isNull()
|
||||
verifyNoInteractions(spatializerRepository)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getDeviceSetting_audioProfileNotEnabled_returnNull() {
|
||||
testScope.runTest {
|
||||
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
|
||||
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(false)
|
||||
|
||||
val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
|
||||
|
||||
assertThat(setting).isNull()
|
||||
verifyNoInteractions(spatializerRepository)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getDeviceSetting_spatialAudioNotSupported_returnNull() {
|
||||
testScope.runTest {
|
||||
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
|
||||
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
|
||||
`when`(
|
||||
spatializerRepository.isSpatialAudioAvailableForDevice(
|
||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
||||
.thenReturn(false)
|
||||
|
||||
val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
|
||||
|
||||
assertThat(setting).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getDeviceSetting_spatialAudioSupported_returnTwoToggles() {
|
||||
testScope.runTest {
|
||||
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
|
||||
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
|
||||
`when`(
|
||||
spatializerRepository.isSpatialAudioAvailableForDevice(
|
||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
||||
.thenReturn(true)
|
||||
`when`(
|
||||
spatializerRepository.isHeadTrackingAvailableForDevice(
|
||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
||||
.thenReturn(false)
|
||||
`when`(spatializerRepository.getSpatialAudioCompatibleDevices())
|
||||
.thenReturn(listOf(BLE_AUDIO_DEVICE_ATTRIBUTES))
|
||||
`when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
|
||||
.thenReturn(false)
|
||||
|
||||
val setting =
|
||||
getLatestValue(underTest.getDeviceSetting(cachedDevice))
|
||||
as DeviceSettingModel.MultiTogglePreference
|
||||
|
||||
assertThat(setting).isNotNull()
|
||||
assertThat(setting.toggles.size).isEqualTo(2)
|
||||
assertThat(setting.state.selectedIndex).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getDeviceSetting_headTrackingSupported_returnThreeToggles() {
|
||||
testScope.runTest {
|
||||
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
|
||||
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
|
||||
`when`(
|
||||
spatializerRepository.isSpatialAudioAvailableForDevice(
|
||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
||||
.thenReturn(true)
|
||||
`when`(
|
||||
spatializerRepository.isHeadTrackingAvailableForDevice(
|
||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
||||
.thenReturn(true)
|
||||
`when`(spatializerRepository.getSpatialAudioCompatibleDevices())
|
||||
.thenReturn(listOf(BLE_AUDIO_DEVICE_ATTRIBUTES))
|
||||
`when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
|
||||
.thenReturn(true)
|
||||
|
||||
val setting =
|
||||
getLatestValue(underTest.getDeviceSetting(cachedDevice))
|
||||
as DeviceSettingModel.MultiTogglePreference
|
||||
|
||||
assertThat(setting).isNotNull()
|
||||
assertThat(setting.toggles.size).isEqualTo(3)
|
||||
assertThat(setting.state.selectedIndex).isEqualTo(2)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getDeviceSetting_updateState_enableSpatialAudio() {
|
||||
testScope.runTest {
|
||||
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
|
||||
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
|
||||
`when`(
|
||||
spatializerRepository.isSpatialAudioAvailableForDevice(
|
||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
||||
.thenReturn(true)
|
||||
`when`(
|
||||
spatializerRepository.isHeadTrackingAvailableForDevice(
|
||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
||||
.thenReturn(true)
|
||||
`when`(spatializerRepository.getSpatialAudioCompatibleDevices()).thenReturn(listOf())
|
||||
`when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
|
||||
.thenReturn(false)
|
||||
|
||||
val setting =
|
||||
getLatestValue(underTest.getDeviceSetting(cachedDevice))
|
||||
as DeviceSettingModel.MultiTogglePreference
|
||||
setting.updateState(DeviceSettingStateModel.MultiTogglePreferenceState(2))
|
||||
runCurrent()
|
||||
|
||||
assertThat(setting).isNotNull()
|
||||
verify(spatializerRepository, times(1))
|
||||
.addSpatialAudioCompatibleDevice(BLE_AUDIO_DEVICE_ATTRIBUTES)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getDeviceSetting_updateState_enableHeadTracking() {
|
||||
testScope.runTest {
|
||||
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
|
||||
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
|
||||
`when`(
|
||||
spatializerRepository.isSpatialAudioAvailableForDevice(
|
||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
||||
.thenReturn(true)
|
||||
`when`(
|
||||
spatializerRepository.isHeadTrackingAvailableForDevice(
|
||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
||||
.thenReturn(true)
|
||||
`when`(spatializerRepository.getSpatialAudioCompatibleDevices()).thenReturn(listOf())
|
||||
`when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
|
||||
.thenReturn(false)
|
||||
|
||||
val setting =
|
||||
getLatestValue(underTest.getDeviceSetting(cachedDevice))
|
||||
as DeviceSettingModel.MultiTogglePreference
|
||||
setting.updateState(DeviceSettingStateModel.MultiTogglePreferenceState(2))
|
||||
runCurrent()
|
||||
|
||||
assertThat(setting).isNotNull()
|
||||
verify(spatializerRepository, times(1))
|
||||
.addSpatialAudioCompatibleDevice(BLE_AUDIO_DEVICE_ATTRIBUTES)
|
||||
verify(spatializerRepository, times(1))
|
||||
.setHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES, true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLatestValue(deviceSettingFlow: Flow<DeviceSettingModel?>): DeviceSettingModel? {
|
||||
var latestValue: DeviceSettingModel? = null
|
||||
deviceSettingFlow.onEach { latestValue = it }.launchIn(testScope.backgroundScope)
|
||||
testScope.runCurrent()
|
||||
return latestValue
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val BLUETOOTH_ADDRESS = "12:34:56:78:12:34"
|
||||
val BLE_AUDIO_DEVICE_ATTRIBUTES =
|
||||
AudioDeviceAttributes(
|
||||
AudioDeviceAttributes.ROLE_OUTPUT,
|
||||
AudioDeviceInfo.TYPE_BLE_HEADSET,
|
||||
BLUETOOTH_ADDRESS,
|
||||
)
|
||||
}
|
||||
}
|
@@ -19,11 +19,13 @@ package com.android.settings.bluetooth.ui.view
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.media.AudioManager
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
|
||||
import com.android.settings.dashboard.DashboardFragment
|
||||
import com.android.settings.testutils.FakeFeatureFactory
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice
|
||||
@@ -31,6 +33,7 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
|
||||
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
|
||||
@@ -45,6 +48,7 @@ import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.eq
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.any
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
import org.mockito.junit.MockitoRule
|
||||
@@ -59,6 +63,7 @@ class DeviceDetailsFragmentFormatterTest {
|
||||
@Mock private lateinit var cachedDevice: CachedBluetoothDevice
|
||||
@Mock private lateinit var bluetoothAdapter: BluetoothAdapter
|
||||
@Mock private lateinit var repository: DeviceSettingRepository
|
||||
@Mock private lateinit var spatialAudioInteractor: SpatialAudioInteractor
|
||||
|
||||
private lateinit var fragment: TestFragment
|
||||
private lateinit var underTest: DeviceDetailsFragmentFormatter
|
||||
@@ -73,6 +78,10 @@ class DeviceDetailsFragmentFormatterTest {
|
||||
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
|
||||
eq(context), eq(bluetoothAdapter), any()))
|
||||
.thenReturn(repository)
|
||||
`when`(
|
||||
featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor(
|
||||
eq(context), any(AudioManager::class.java), any()))
|
||||
.thenReturn(spatialAudioInteractor)
|
||||
val fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java)
|
||||
assertThat(fragmentActivity.applicationContext).isNotNull()
|
||||
fragment = TestFragment(context)
|
||||
@@ -186,7 +195,15 @@ class DeviceDetailsFragmentFormatterTest {
|
||||
toggles =
|
||||
listOf(
|
||||
ToggleModel(
|
||||
"", Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))),
|
||||
"", DeviceSettingIcon.BitmapIcon(
|
||||
Bitmap.createBitmap(
|
||||
1,
|
||||
1,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
isActive = true,
|
||||
state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
|
||||
isAllowedChangingState = true,
|
||||
|
@@ -20,6 +20,7 @@ import android.bluetooth.BluetoothAdapter
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
|
||||
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
|
||||
import com.android.settings.testutils.FakeFeatureFactory
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice
|
||||
@@ -27,6 +28,7 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
|
||||
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
|
||||
@@ -45,6 +47,8 @@ import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.eq
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.times
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
import org.mockito.junit.MockitoRule
|
||||
@@ -61,6 +65,8 @@ class BluetoothDeviceDetailsViewModelTest {
|
||||
|
||||
@Mock private lateinit var repository: DeviceSettingRepository
|
||||
|
||||
@Mock private lateinit var spatialAudioInteractor: SpatialAudioInteractor
|
||||
|
||||
private lateinit var underTest: BluetoothDeviceDetailsViewModel
|
||||
private lateinit var featureFactory: FakeFeatureFactory
|
||||
private val testScope = TestScope()
|
||||
@@ -74,7 +80,8 @@ class BluetoothDeviceDetailsViewModelTest {
|
||||
eq(context), eq(bluetoothAdapter), any()))
|
||||
.thenReturn(repository)
|
||||
|
||||
underTest = BluetoothDeviceDetailsViewModel(repository, cachedDevice)
|
||||
underTest =
|
||||
BluetoothDeviceDetailsViewModel(repository, spatialAudioInteractor, cachedDevice)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -91,6 +98,66 @@ class BluetoothDeviceDetailsViewModelTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getDeviceSetting_returnRepositoryResponse() {
|
||||
testScope.runTest {
|
||||
val remoteSettingId1 = 10001
|
||||
val pref = buildMultiTogglePreference(remoteSettingId1)
|
||||
`when`(repository.getDeviceSettingsConfig(cachedDevice))
|
||||
.thenReturn(
|
||||
DeviceSettingConfigModel(
|
||||
listOf(
|
||||
BUILTIN_SETTING_ITEM_1,
|
||||
buildRemoteSettingItem(remoteSettingId1),
|
||||
),
|
||||
listOf(),
|
||||
"footer"))
|
||||
`when`(repository.getDeviceSetting(cachedDevice, remoteSettingId1))
|
||||
.thenReturn(flowOf(pref))
|
||||
|
||||
var deviceSetting: DeviceSettingModel? = null
|
||||
underTest
|
||||
.getDeviceSetting(cachedDevice, remoteSettingId1)
|
||||
.onEach { deviceSetting = it }
|
||||
.launchIn(testScope.backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
assertThat(deviceSetting).isSameInstanceAs(pref)
|
||||
verify(repository, times(1)).getDeviceSetting(cachedDevice, remoteSettingId1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getDeviceSetting_spatialAudio_returnSpatialAudioInteractorResponse() {
|
||||
testScope.runTest {
|
||||
val pref =
|
||||
buildMultiTogglePreference(
|
||||
DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE)
|
||||
`when`(repository.getDeviceSettingsConfig(cachedDevice))
|
||||
.thenReturn(
|
||||
DeviceSettingConfigModel(
|
||||
listOf(
|
||||
BUILTIN_SETTING_ITEM_1,
|
||||
buildRemoteSettingItem(
|
||||
DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE),
|
||||
),
|
||||
listOf(),
|
||||
"footer"))
|
||||
`when`(spatialAudioInteractor.getDeviceSetting(cachedDevice)).thenReturn(flowOf(pref))
|
||||
|
||||
var deviceSetting: DeviceSettingModel? = null
|
||||
underTest
|
||||
.getDeviceSetting(
|
||||
cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE)
|
||||
.onEach { deviceSetting = it }
|
||||
.launchIn(testScope.backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
assertThat(deviceSetting).isSameInstanceAs(pref)
|
||||
verify(spatialAudioInteractor, times(1)).getDeviceSetting(cachedDevice)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getLayout_builtinDeviceSettings() {
|
||||
testScope.runTest {
|
||||
@@ -163,7 +230,12 @@ class BluetoothDeviceDetailsViewModelTest {
|
||||
cachedDevice,
|
||||
settingId,
|
||||
"title",
|
||||
toggles = listOf(ToggleModel("", Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))),
|
||||
toggles =
|
||||
listOf(
|
||||
ToggleModel(
|
||||
"toggle1",
|
||||
DeviceSettingIcon.BitmapIcon(
|
||||
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)))),
|
||||
isActive = true,
|
||||
state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
|
||||
isAllowedChangingState = true,
|
||||
|
Reference in New Issue
Block a user