Fix coroutine scope expired and UI animation issue
BUG: 375365790
BUG: 375146578
BUG: 375304695
BUG: 375544752
Test: atest BluetoothDeviceDetailsViewModelTest
Flag: com.android.settings.flags.enable_bluetooth_device_details_polish
Change-Id: Ib3bc6699f256288b6c4995b78cc25a16f1af0792
diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
index 0e51d17..2860ce8 100644
--- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
@@ -421,11 +421,13 @@
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
List<String> invisibleProfiles = List.of();
if (Flags.enableBluetoothDeviceDetailsPolish()) {
- mFormatter =
- FeatureFactory.getFeatureFactory()
- .getBluetoothFeatureProvider()
- .getDeviceDetailsFragmentFormatter(
- requireContext(), this, mBluetoothAdapter, mCachedDevice);
+ if (mFormatter == null) {
+ mFormatter =
+ FeatureFactory.getFeatureFactory()
+ .getBluetoothFeatureProvider()
+ .getDeviceDetailsFragmentFormatter(
+ requireContext(), this, mBluetoothAdapter, mCachedDevice);
+ }
invisibleProfiles =
mFormatter.getInvisibleBluetoothProfiles(
FragmentTypeModel.DeviceDetailsMainFragment.INSTANCE);
diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java b/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java
index be0f6f3..1bad5e5 100644
--- a/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java
+++ b/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java
@@ -25,7 +25,6 @@
import android.net.Uri;
import androidx.annotation.NonNull;
-import androidx.lifecycle.LifecycleCoroutineScope;
import androidx.preference.Preference;
import com.android.settings.SettingsPreferenceFragment;
@@ -34,12 +33,12 @@
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository;
+import kotlinx.coroutines.CoroutineScope;
+
import java.util.List;
import java.util.Set;
-/**
- * Provider for bluetooth related features.
- */
+/** Provider for bluetooth related features. */
public interface BluetoothFeatureProvider {
/**
@@ -86,26 +85,25 @@
/**
* Gets the bluetooth profile preference keys which should be hidden in the device details page.
*
- * @param context Context
+ * @param context Context
* @param bluetoothDevice the bluetooth device
* @return the profiles which should be hidden
*/
- Set<String> getInvisibleProfilePreferenceKeys(
- Context context, BluetoothDevice bluetoothDevice);
+ Set<String> getInvisibleProfilePreferenceKeys(Context context, BluetoothDevice bluetoothDevice);
/** Gets DeviceSettingRepository. */
@NonNull
DeviceSettingRepository getDeviceSettingRepository(
@NonNull Context context,
@NonNull BluetoothAdapter bluetoothAdapter,
- @NonNull LifecycleCoroutineScope scope);
+ @NonNull CoroutineScope scope);
/** Gets spatial audio interactor. */
@NonNull
SpatialAudioInteractor getSpatialAudioInteractor(
@NonNull Context context,
@NonNull AudioManager audioManager,
- @NonNull LifecycleCoroutineScope scope);
+ @NonNull CoroutineScope scope);
/** Gets device details fragment layout formatter. */
@NonNull
diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.kt b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.kt
index 25c586e..6f967a2 100644
--- a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.kt
+++ b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.kt
@@ -22,6 +22,7 @@
import android.media.AudioManager
import android.media.Spatializer
import android.net.Uri
+import android.util.Log
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.preference.Preference
import com.android.settings.SettingsPreferenceFragment
@@ -37,6 +38,7 @@
import com.android.settingslib.media.domain.interactor.SpatializerInteractor
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
/** Impl of [BluetoothFeatureProvider] */
@@ -76,14 +78,14 @@
override fun getDeviceSettingRepository(
context: Context,
bluetoothAdapter: BluetoothAdapter,
- scope: LifecycleCoroutineScope
+ scope: CoroutineScope
): DeviceSettingRepository =
DeviceSettingRepositoryImpl(context, bluetoothAdapter, scope, Dispatchers.IO)
override fun getSpatialAudioInteractor(
context: Context,
audioManager: AudioManager,
- scope: LifecycleCoroutineScope
+ scope: CoroutineScope,
): SpatialAudioInteractor {
return SpatialAudioInteractorImpl(
context, audioManager,
diff --git a/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt b/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt
index 6b72b53..4b91716a 100644
--- a/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt
+++ b/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt
@@ -147,7 +147,7 @@
}
companion object {
- private const val TAG = "SpatialAudioInteractorImpl"
+ private const val TAG = "SpatialAudioInteractor"
private const val INDEX_SPATIAL_AUDIO_OFF = 0
private const val INDEX_SPATIAL_AUDIO_ON = 1
private const val INDEX_HEAD_TRACKING_ENABLED = 2
diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt
index ad4176f..13c3b50 100644
--- a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt
+++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt
@@ -19,11 +19,10 @@
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.content.Intent
-import android.media.AudioManager
import android.os.Bundle
import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.expandVertically
-import androidx.compose.animation.shrinkVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
@@ -33,14 +32,12 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import com.android.settings.R
import com.android.settings.SettingsPreferenceFragment
@@ -52,7 +49,6 @@
import com.android.settings.bluetooth.ui.view.DeviceDetailsMoreSettingsFragment.Companion.KEY_DEVICE_ADDRESS
import com.android.settings.bluetooth.ui.viewmodel.BluetoothDeviceDetailsViewModel
import com.android.settings.core.SubSettingLauncher
-import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
import com.android.settings.spa.preference.ComposePreference
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingActionModel
@@ -97,29 +93,17 @@
class DeviceDetailsFragmentFormatterImpl(
private val context: Context,
private val fragment: SettingsPreferenceFragment,
- bluetoothAdapter: BluetoothAdapter,
+ private val bluetoothAdapter: BluetoothAdapter,
private val cachedDevice: CachedBluetoothDevice,
private val backgroundCoroutineContext: CoroutineContext,
) : DeviceDetailsFragmentFormatter {
- private val repository =
- featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
- fragment.requireActivity().application,
- bluetoothAdapter,
- fragment.lifecycleScope,
- )
- private val spatialAudioInteractor =
- featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor(
- fragment.requireActivity().application,
- context.getSystemService(AudioManager::class.java),
- fragment.lifecycleScope,
- )
+
private val viewModel: BluetoothDeviceDetailsViewModel =
ViewModelProvider(
fragment,
BluetoothDeviceDetailsViewModel.Factory(
fragment.requireActivity().application,
- repository,
- spatialAudioInteractor,
+ bluetoothAdapter,
cachedDevice,
backgroundCoroutineContext,
),
@@ -224,8 +208,8 @@
val settings = contents
AnimatedVisibility(
visible = settings.isNotEmpty(),
- enter = expandVertically(expandFrom = Alignment.Top),
- exit = shrinkVertically(shrinkTowards = Alignment.Top),
+ enter = fadeIn(),
+ exit = fadeOut(),
) {
Box {
Box(
diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsMoreSettingsFragment.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsMoreSettingsFragment.kt
index 7cb1c0d..66fba70 100644
--- a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsMoreSettingsFragment.kt
+++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsMoreSettingsFragment.kt
@@ -120,13 +120,15 @@
finish()
return emptyList()
}
- formatter =
- featureFactory.bluetoothFeatureProvider.getDeviceDetailsFragmentFormatter(
- requireContext(),
- this,
- bluetoothManager.adapter,
- cachedDevice,
- )
+ if (!this::formatter.isInitialized) {
+ formatter =
+ featureFactory.bluetoothFeatureProvider.getDeviceDetailsFragmentFormatter(
+ requireContext(),
+ this,
+ bluetoothManager.adapter,
+ cachedDevice,
+ )
+ }
helpItem =
formatter
.getMenuItem(FragmentTypeModel.DeviceDetailsMoreSettingsFragment)
diff --git a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt
index 3b7a582..1ea2da3 100644
--- a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt
+++ b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt
@@ -17,20 +17,22 @@
package com.android.settings.bluetooth.ui.viewmodel
import android.app.Application
+import android.bluetooth.BluetoothAdapter
+import android.media.AudioManager
+import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.android.settings.R
-import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutColumn
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow
import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel
+import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
import com.android.settingslib.bluetooth.CachedBluetoothDevice
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.DeviceSettingIcon
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
@@ -47,12 +49,24 @@
class BluetoothDeviceDetailsViewModel(
private val application: Application,
- private val deviceSettingRepository: DeviceSettingRepository,
- private val spatialAudioInteractor: SpatialAudioInteractor,
+ private val bluetoothAdapter: BluetoothAdapter,
private val cachedDevice: CachedBluetoothDevice,
backgroundCoroutineContext: CoroutineContext,
) : AndroidViewModel(application) {
+ private val deviceSettingRepository =
+ featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
+ application,
+ bluetoothAdapter,
+ viewModelScope,
+ )
+ private val spatialAudioInteractor =
+ featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor(
+ application,
+ application.getSystemService(AudioManager::class.java),
+ viewModelScope,
+ )
+
private val items =
viewModelScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) {
deviceSettingRepository.getDeviceSettingsConfig(cachedDevice)
@@ -202,8 +216,7 @@
class Factory(
private val application: Application,
- private val deviceSettingRepository: DeviceSettingRepository,
- private val spatialAudioInteractor: SpatialAudioInteractor,
+ private val bluetoothAdapter: BluetoothAdapter,
private val cachedDevice: CachedBluetoothDevice,
private val backgroundCoroutineContext: CoroutineContext,
) : ViewModelProvider.Factory {
@@ -211,8 +224,7 @@
@Suppress("UNCHECKED_CAST")
return BluetoothDeviceDetailsViewModel(
application,
- deviceSettingRepository,
- spatialAudioInteractor,
+ bluetoothAdapter,
cachedDevice,
backgroundCoroutineContext,
)
diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt
index c3f938c..6813d94 100644
--- a/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt
+++ b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt
@@ -19,6 +19,7 @@
import android.app.Application
import android.bluetooth.BluetoothAdapter
import android.graphics.Bitmap
+import android.media.AudioManager
import androidx.test.core.app.ApplicationProvider
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
@@ -46,7 +47,9 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
+import org.mockito.Mockito.any
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
@@ -76,11 +79,21 @@
val application = ApplicationProvider.getApplicationContext<Application>()
featureFactory = FakeFeatureFactory.setupForTest()
+ `when`(
+ featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
+ eq(application), eq(bluetoothAdapter), any()
+ ))
+ .thenReturn(repository)
+ `when`(
+ featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor(
+ eq(application), any(AudioManager::class.java), any()
+ ))
+ .thenReturn(spatialAudioInteractor)
+
underTest =
BluetoothDeviceDetailsViewModel(
application,
- repository,
- spatialAudioInteractor,
+ bluetoothAdapter,
cachedDevice,
testScope.testScheduler)
}