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
This commit is contained in:
Haijie Hong
2024-10-27 23:30:04 +08:00
parent ab9f9780ab
commit 7764a3e5af
8 changed files with 70 additions and 57 deletions

View File

@@ -421,11 +421,13 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
List<String> invisibleProfiles = List.of(); List<String> invisibleProfiles = List.of();
if (Flags.enableBluetoothDeviceDetailsPolish()) { if (Flags.enableBluetoothDeviceDetailsPolish()) {
if (mFormatter == null) {
mFormatter = mFormatter =
FeatureFactory.getFeatureFactory() FeatureFactory.getFeatureFactory()
.getBluetoothFeatureProvider() .getBluetoothFeatureProvider()
.getDeviceDetailsFragmentFormatter( .getDeviceDetailsFragmentFormatter(
requireContext(), this, mBluetoothAdapter, mCachedDevice); requireContext(), this, mBluetoothAdapter, mCachedDevice);
}
invisibleProfiles = invisibleProfiles =
mFormatter.getInvisibleBluetoothProfiles( mFormatter.getInvisibleBluetoothProfiles(
FragmentTypeModel.DeviceDetailsMainFragment.INSTANCE); FragmentTypeModel.DeviceDetailsMainFragment.INSTANCE);

View File

@@ -25,7 +25,6 @@ import android.media.Spatializer;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleCoroutineScope;
import androidx.preference.Preference; import androidx.preference.Preference;
import com.android.settings.SettingsPreferenceFragment; import com.android.settings.SettingsPreferenceFragment;
@@ -34,12 +33,12 @@ import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository; import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository;
import kotlinx.coroutines.CoroutineScope;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
/** /** Provider for bluetooth related features. */
* Provider for bluetooth related features.
*/
public interface BluetoothFeatureProvider { public interface BluetoothFeatureProvider {
/** /**
@@ -90,22 +89,21 @@ public interface BluetoothFeatureProvider {
* @param bluetoothDevice the bluetooth device * @param bluetoothDevice the bluetooth device
* @return the profiles which should be hidden * @return the profiles which should be hidden
*/ */
Set<String> getInvisibleProfilePreferenceKeys( Set<String> getInvisibleProfilePreferenceKeys(Context context, BluetoothDevice bluetoothDevice);
Context context, BluetoothDevice bluetoothDevice);
/** Gets DeviceSettingRepository. */ /** Gets DeviceSettingRepository. */
@NonNull @NonNull
DeviceSettingRepository getDeviceSettingRepository( DeviceSettingRepository getDeviceSettingRepository(
@NonNull Context context, @NonNull Context context,
@NonNull BluetoothAdapter bluetoothAdapter, @NonNull BluetoothAdapter bluetoothAdapter,
@NonNull LifecycleCoroutineScope scope); @NonNull CoroutineScope scope);
/** Gets spatial audio interactor. */ /** Gets spatial audio interactor. */
@NonNull @NonNull
SpatialAudioInteractor getSpatialAudioInteractor( SpatialAudioInteractor getSpatialAudioInteractor(
@NonNull Context context, @NonNull Context context,
@NonNull AudioManager audioManager, @NonNull AudioManager audioManager,
@NonNull LifecycleCoroutineScope scope); @NonNull CoroutineScope scope);
/** Gets device details fragment layout formatter. */ /** Gets device details fragment layout formatter. */
@NonNull @NonNull

View File

@@ -22,6 +22,7 @@ import android.content.Context
import android.media.AudioManager import android.media.AudioManager
import android.media.Spatializer import android.media.Spatializer
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.preference.Preference import androidx.preference.Preference
import com.android.settings.SettingsPreferenceFragment import com.android.settings.SettingsPreferenceFragment
@@ -37,6 +38,7 @@ import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl
import com.android.settingslib.media.domain.interactor.SpatializerInteractor import com.android.settingslib.media.domain.interactor.SpatializerInteractor
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
/** Impl of [BluetoothFeatureProvider] */ /** Impl of [BluetoothFeatureProvider] */
@@ -76,14 +78,14 @@ open class BluetoothFeatureProviderImpl : BluetoothFeatureProvider {
override fun getDeviceSettingRepository( override fun getDeviceSettingRepository(
context: Context, context: Context,
bluetoothAdapter: BluetoothAdapter, bluetoothAdapter: BluetoothAdapter,
scope: LifecycleCoroutineScope scope: CoroutineScope
): DeviceSettingRepository = ): DeviceSettingRepository =
DeviceSettingRepositoryImpl(context, bluetoothAdapter, scope, Dispatchers.IO) DeviceSettingRepositoryImpl(context, bluetoothAdapter, scope, Dispatchers.IO)
override fun getSpatialAudioInteractor( override fun getSpatialAudioInteractor(
context: Context, context: Context,
audioManager: AudioManager, audioManager: AudioManager,
scope: LifecycleCoroutineScope scope: CoroutineScope,
): SpatialAudioInteractor { ): SpatialAudioInteractor {
return SpatialAudioInteractorImpl( return SpatialAudioInteractorImpl(
context, audioManager, context, audioManager,

View File

@@ -147,7 +147,7 @@ class SpatialAudioInteractorImpl(
} }
companion object { 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_OFF = 0
private const val INDEX_SPATIAL_AUDIO_ON = 1 private const val INDEX_SPATIAL_AUDIO_ON = 1
private const val INDEX_HEAD_TRACKING_ENABLED = 2 private const val INDEX_HEAD_TRACKING_ENABLED = 2

View File

@@ -19,11 +19,10 @@ package com.android.settings.bluetooth.ui.view
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.media.AudioManager
import android.os.Bundle import android.os.Bundle
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -33,14 +32,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import com.android.settings.R import com.android.settings.R
import com.android.settings.SettingsPreferenceFragment import com.android.settings.SettingsPreferenceFragment
@@ -52,7 +49,6 @@ import com.android.settings.bluetooth.ui.model.FragmentTypeModel
import com.android.settings.bluetooth.ui.view.DeviceDetailsMoreSettingsFragment.Companion.KEY_DEVICE_ADDRESS import com.android.settings.bluetooth.ui.view.DeviceDetailsMoreSettingsFragment.Companion.KEY_DEVICE_ADDRESS
import com.android.settings.bluetooth.ui.viewmodel.BluetoothDeviceDetailsViewModel import com.android.settings.bluetooth.ui.viewmodel.BluetoothDeviceDetailsViewModel
import com.android.settings.core.SubSettingLauncher import com.android.settings.core.SubSettingLauncher
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
import com.android.settings.spa.preference.ComposePreference import com.android.settings.spa.preference.ComposePreference
import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingActionModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingActionModel
@@ -97,29 +93,17 @@ interface DeviceDetailsFragmentFormatter {
class DeviceDetailsFragmentFormatterImpl( class DeviceDetailsFragmentFormatterImpl(
private val context: Context, private val context: Context,
private val fragment: SettingsPreferenceFragment, private val fragment: SettingsPreferenceFragment,
bluetoothAdapter: BluetoothAdapter, private val bluetoothAdapter: BluetoothAdapter,
private val cachedDevice: CachedBluetoothDevice, private val cachedDevice: CachedBluetoothDevice,
private val backgroundCoroutineContext: CoroutineContext, private val backgroundCoroutineContext: CoroutineContext,
) : DeviceDetailsFragmentFormatter { ) : 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 = private val viewModel: BluetoothDeviceDetailsViewModel =
ViewModelProvider( ViewModelProvider(
fragment, fragment,
BluetoothDeviceDetailsViewModel.Factory( BluetoothDeviceDetailsViewModel.Factory(
fragment.requireActivity().application, fragment.requireActivity().application,
repository, bluetoothAdapter,
spatialAudioInteractor,
cachedDevice, cachedDevice,
backgroundCoroutineContext, backgroundCoroutineContext,
), ),
@@ -224,8 +208,8 @@ class DeviceDetailsFragmentFormatterImpl(
val settings = contents val settings = contents
AnimatedVisibility( AnimatedVisibility(
visible = settings.isNotEmpty(), visible = settings.isNotEmpty(),
enter = expandVertically(expandFrom = Alignment.Top), enter = fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Top), exit = fadeOut(),
) { ) {
Box { Box {
Box( Box(

View File

@@ -120,6 +120,7 @@ class DeviceDetailsMoreSettingsFragment : DashboardFragment() {
finish() finish()
return emptyList() return emptyList()
} }
if (!this::formatter.isInitialized) {
formatter = formatter =
featureFactory.bluetoothFeatureProvider.getDeviceDetailsFragmentFormatter( featureFactory.bluetoothFeatureProvider.getDeviceDetailsFragmentFormatter(
requireContext(), requireContext(),
@@ -127,6 +128,7 @@ class DeviceDetailsMoreSettingsFragment : DashboardFragment() {
bluetoothManager.adapter, bluetoothManager.adapter,
cachedDevice, cachedDevice,
) )
}
helpItem = helpItem =
formatter formatter
.getMenuItem(FragmentTypeModel.DeviceDetailsMoreSettingsFragment) .getMenuItem(FragmentTypeModel.DeviceDetailsMoreSettingsFragment)

View File

@@ -17,20 +17,22 @@
package com.android.settings.bluetooth.ui.viewmodel package com.android.settings.bluetooth.ui.viewmodel
import android.app.Application import android.app.Application
import android.bluetooth.BluetoothAdapter
import android.media.AudioManager
import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.android.settings.R 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.DeviceSettingLayout
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutColumn import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutColumn
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow
import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel 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.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId 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.DeviceSettingConfigItemModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon 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.DeviceSettingModel
@@ -47,12 +49,24 @@ import kotlinx.coroutines.flow.stateIn
class BluetoothDeviceDetailsViewModel( class BluetoothDeviceDetailsViewModel(
private val application: Application, private val application: Application,
private val deviceSettingRepository: DeviceSettingRepository, private val bluetoothAdapter: BluetoothAdapter,
private val spatialAudioInteractor: SpatialAudioInteractor,
private val cachedDevice: CachedBluetoothDevice, private val cachedDevice: CachedBluetoothDevice,
backgroundCoroutineContext: CoroutineContext, backgroundCoroutineContext: CoroutineContext,
) : AndroidViewModel(application) { ) : 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 = private val items =
viewModelScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) { viewModelScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) {
deviceSettingRepository.getDeviceSettingsConfig(cachedDevice) deviceSettingRepository.getDeviceSettingsConfig(cachedDevice)
@@ -202,8 +216,7 @@ class BluetoothDeviceDetailsViewModel(
class Factory( class Factory(
private val application: Application, private val application: Application,
private val deviceSettingRepository: DeviceSettingRepository, private val bluetoothAdapter: BluetoothAdapter,
private val spatialAudioInteractor: SpatialAudioInteractor,
private val cachedDevice: CachedBluetoothDevice, private val cachedDevice: CachedBluetoothDevice,
private val backgroundCoroutineContext: CoroutineContext, private val backgroundCoroutineContext: CoroutineContext,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@@ -211,8 +224,7 @@ class BluetoothDeviceDetailsViewModel(
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return BluetoothDeviceDetailsViewModel( return BluetoothDeviceDetailsViewModel(
application, application,
deviceSettingRepository, bluetoothAdapter,
spatialAudioInteractor,
cachedDevice, cachedDevice,
backgroundCoroutineContext, backgroundCoroutineContext,
) )

View File

@@ -19,6 +19,7 @@ package com.android.settings.bluetooth.ui.viewmodel
import android.app.Application import android.app.Application
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.graphics.Bitmap import android.graphics.Bitmap
import android.media.AudioManager
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
@@ -46,7 +47,9 @@ import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.times import org.mockito.Mockito.times
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
@@ -76,11 +79,21 @@ class BluetoothDeviceDetailsViewModelTest {
val application = ApplicationProvider.getApplicationContext<Application>() val application = ApplicationProvider.getApplicationContext<Application>()
featureFactory = FakeFeatureFactory.setupForTest() 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 = underTest =
BluetoothDeviceDetailsViewModel( BluetoothDeviceDetailsViewModel(
application, application,
repository, bluetoothAdapter,
spatialAudioInteractor,
cachedDevice, cachedDevice,
testScope.testScheduler) testScope.testScheduler)
} }