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)
     }