Loading packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt +98 −42 Original line number Diff line number Diff line Loading @@ -19,37 +19,39 @@ package com.android.settingslib.bluetooth.devicesettings.data.repository import android.bluetooth.BluetoothAdapter import android.content.Context import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreference import com.android.settingslib.bluetooth.devicesettings.DeviceSetting import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId import com.android.settingslib.bluetooth.devicesettings.DeviceSettingPreferenceState import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig import java.util.concurrent.ConcurrentHashMap import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference import com.android.settingslib.bluetooth.devicesettings.ToggleInfo 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.DeviceSettingModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel import com.google.common.cache.CacheBuilder import com.google.common.cache.CacheLoader import com.google.common.cache.LoadingCache import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch /** Provides functionality to control bluetooth device settings. */ interface DeviceSettingRepository { /** Gets config for the bluetooth device, returns null if failed. */ suspend fun getDeviceSettingsConfig(cachedDevice: CachedBluetoothDevice): DeviceSettingsConfig? /** Gets all device settings for the bluetooth device. */ fun getDeviceSettingList( cachedDevice: CachedBluetoothDevice, ): Flow<List<DeviceSetting>?> suspend fun getDeviceSettingsConfig( cachedDevice: CachedBluetoothDevice ): DeviceSettingConfigModel? /** Gets device setting for the bluetooth device. */ fun getDeviceSetting( cachedDevice: CachedBluetoothDevice, @DeviceSettingId settingId: Int ): Flow<DeviceSetting?> /** Updates device setting for the bluetooth device. */ suspend fun updateDeviceSettingState( cachedDevice: CachedBluetoothDevice, @DeviceSettingId deviceSettingId: Int, deviceSettingPreferenceState: DeviceSettingPreferenceState, ) ): Flow<DeviceSettingModel?> } class DeviceSettingRepositoryImpl( Loading @@ -58,40 +60,94 @@ class DeviceSettingRepositoryImpl( private val coroutineScope: CoroutineScope, private val backgroundCoroutineContext: CoroutineContext, ) : DeviceSettingRepository { private val deviceSettings = ConcurrentHashMap<CachedBluetoothDevice, DeviceSettingServiceConnection>() override suspend fun getDeviceSettingsConfig( private val connectionCache: LoadingCache<CachedBluetoothDevice, DeviceSettingServiceConnection> = CacheBuilder.newBuilder() .weakValues() .build( object : CacheLoader<CachedBluetoothDevice, DeviceSettingServiceConnection>() { override fun load( cachedDevice: CachedBluetoothDevice ): DeviceSettingsConfig? = createConnectionIfAbsent(cachedDevice).getDeviceSettingsConfig() ): DeviceSettingServiceConnection = DeviceSettingServiceConnection( cachedDevice, context, bluetoothAdaptor, coroutineScope, backgroundCoroutineContext, ) } ) override fun getDeviceSettingList( override suspend fun getDeviceSettingsConfig( cachedDevice: CachedBluetoothDevice ): Flow<List<DeviceSetting>?> = createConnectionIfAbsent(cachedDevice).getDeviceSettingList() ): DeviceSettingConfigModel? = connectionCache.get(cachedDevice).getDeviceSettingsConfig()?.toModel() override fun getDeviceSetting( cachedDevice: CachedBluetoothDevice, settingId: Int ): Flow<DeviceSetting?> = createConnectionIfAbsent(cachedDevice).getDeviceSetting(settingId) ): Flow<DeviceSettingModel?> = connectionCache.get(cachedDevice).let { connection -> connection.getDeviceSetting(settingId).map { it?.toModel(cachedDevice, connection) } } override suspend fun updateDeviceSettingState( cachedDevice: CachedBluetoothDevice, @DeviceSettingId deviceSettingId: Int, deviceSettingPreferenceState: DeviceSettingPreferenceState, ) = createConnectionIfAbsent(cachedDevice) .updateDeviceSettings(deviceSettingId, deviceSettingPreferenceState) private fun DeviceSettingsConfig.toModel(): DeviceSettingConfigModel = DeviceSettingConfigModel( mainItems = mainContentItems.map { it.toModel() }, moreSettingsItems = moreSettingsItems.map { it.toModel() }, moreSettingsPageFooter = moreSettingsFooter ) private fun createConnectionIfAbsent( cachedDevice: CachedBluetoothDevice ): DeviceSettingServiceConnection = deviceSettings.computeIfAbsent(cachedDevice) { DeviceSettingServiceConnection( cachedDevice, context, bluetoothAdaptor, coroutineScope, backgroundCoroutineContext, private fun DeviceSettingItem.toModel(): DeviceSettingConfigItemModel = DeviceSettingConfigItemModel(settingId) private fun DeviceSetting.toModel( cachedDevice: CachedBluetoothDevice, connection: DeviceSettingServiceConnection ): DeviceSettingModel = when (val pref = preference) { is ActionSwitchPreference -> DeviceSettingModel.ActionSwitchPreference( cachedDevice = cachedDevice, id = settingId, title = pref.title, summary = pref.summary, icon = pref.icon, isAllowedChangingState = pref.isAllowedChangingState, intent = pref.intent, switchState = if (pref.hasSwitch()) { DeviceSettingStateModel.ActionSwitchPreferenceState(pref.checked) } else { null }, updateState = { newState -> coroutineScope.launch(backgroundCoroutineContext) { connection.updateDeviceSettings( settingId, newState.toParcelable(), ) } }, ) is MultiTogglePreference -> DeviceSettingModel.MultiTogglePreference( cachedDevice = cachedDevice, id = settingId, title = pref.title, toggles = pref.toggleInfos.map { it.toModel() }, isAllowedChangingState = pref.isAllowedChangingState, isActive = true, state = DeviceSettingStateModel.MultiTogglePreferenceState(pref.state), updateState = { newState -> coroutineScope.launch(backgroundCoroutineContext) { connection.updateDeviceSettings(settingId, newState.toParcelable()) } }, ) else -> DeviceSettingModel.Unknown(cachedDevice, settingId) } private fun ToggleInfo.toModel(): ToggleModel = ToggleModel(label, icon) } packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt 0 → 100644 +33 −0 Original line number Diff line number Diff line /* * 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.settingslib.bluetooth.devicesettings.shared.model import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId /** Models a device setting config. */ data class DeviceSettingConfigModel( /** Items need to be shown in device details main page. */ val mainItems: List<DeviceSettingConfigItemModel>, /** Items need to be shown in device details more settings page. */ val moreSettingsItems: List<DeviceSettingConfigItemModel>, /** Footer text in more settings page. */ val moreSettingsPageFooter: String) /** Models a device setting item in config. */ data class DeviceSettingConfigItemModel( @DeviceSettingId val settingId: Int, ) packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt +143 −49 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.graphics.Bitmap import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreference import com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreferenceState Loading @@ -34,6 +35,14 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreferenceState import com.android.settingslib.bluetooth.devicesettings.ToggleInfo 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.DeviceSettingModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay Loading Loading @@ -148,7 +157,7 @@ class DeviceSettingRepositoryTest { val config = underTest.getDeviceSettingsConfig(cachedDevice) assertThat(config).isSameInstanceAs(DEVICE_SETTING_CONFIG) assertConfig(config!!, DEVICE_SETTING_CONFIG) } } Loading @@ -163,7 +172,7 @@ class DeviceSettingRepositoryTest { ) .thenReturn("".toByteArray()) var config: DeviceSettingsConfig? = null var config: DeviceSettingConfigModel? = null val job = launch { config = underTest.getDeviceSettingsConfig(cachedDevice) } delay(1000) verify(bluetoothAdapter) Loading @@ -185,7 +194,7 @@ class DeviceSettingRepositoryTest { .thenReturn(BLUETOOTH_DEVICE_METADATA.toByteArray()) job.join() assertThat(config).isSameInstanceAs(DEVICE_SETTING_CONFIG) assertConfig(config!!, DEVICE_SETTING_CONFIG) } } Loading @@ -202,7 +211,7 @@ class DeviceSettingRepositoryTest { } @Test fun getDeviceSettingList_success() { fun getDeviceSetting_actionSwitchPreference_success() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { Loading @@ -211,64 +220,63 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) } var setting: DeviceSettingModel? = null underTest .getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_HEADER) .onEach { setting = it } .launchIn(backgroundScope) runCurrent() assertDeviceSetting(setting!!, DEVICE_SETTING_1) } } @Test fun getDeviceSetting_multiTogglePreference_success() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then { input -> input .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_2)) } var settings: List<DeviceSetting>? = null var setting: DeviceSettingModel? = null underTest .getDeviceSettingList(cachedDevice) .onEach { settings = it } .getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC) .onEach { setting = it } .launchIn(backgroundScope) runCurrent() assertThat(settings?.map { it.settingId }) .containsExactly( DeviceSettingId.DEVICE_SETTING_ID_HEADER, DeviceSettingId.DEVICE_SETTING_ID_ANC ) assertThat(settings?.map { (it.preference as ActionSwitchPreference).title }) .containsExactly( "title1", "title2", ) assertDeviceSetting(setting!!, DEVICE_SETTING_2) } } @Test fun getDeviceSetting_oneServiceFailed_returnPartialResult() { fun getDeviceSetting_noConfig_returnNull() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { input -> input .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) } var settings: List<DeviceSetting>? = null var setting: DeviceSettingModel? = null underTest .getDeviceSettingList(cachedDevice) .onEach { settings = it } .getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_HEADER) .onEach { setting = it } .launchIn(backgroundScope) runCurrent() assertThat(settings?.map { it.settingId }) .containsExactly( DeviceSettingId.DEVICE_SETTING_ID_HEADER, ) assertThat(settings?.map { (it.preference as ActionSwitchPreference).title }) .containsExactly( "title1", ) assertThat(setting).isNull() } } @Test fun getDeviceSetting_success() { fun updateDeviceSettingState_switchState_success() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { Loading @@ -277,48 +285,123 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) } var setting: DeviceSetting? = null var setting: DeviceSettingModel? = null underTest .getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_HEADER) .onEach { setting = it } .launchIn(backgroundScope) runCurrent() val updateFunc = (setting as DeviceSettingModel.ActionSwitchPreference).updateState!! updateFunc(DeviceSettingStateModel.ActionSwitchPreferenceState(false)) runCurrent() assertThat(setting?.settingId).isEqualTo(DeviceSettingId.DEVICE_SETTING_ID_HEADER) assertThat((setting?.preference as ActionSwitchPreference).title).isEqualTo("title1") verify(settingProviderService1) .updateDeviceSettings( DEVICE_INFO, DeviceSettingState.Builder() .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER) .setPreferenceState( ActionSwitchPreferenceState.Builder().setChecked(false).build() ) .build() ) } } @Test fun updateDeviceSetting_success() { fun updateDeviceSettingState_multiToggleState_success() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then { input -> input .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_2)) } var setting: DeviceSettingModel? = null underTest.updateDeviceSettingState( cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_HEADER, ActionSwitchPreferenceState.Builder().build() ) underTest .getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC) .onEach { setting = it } .launchIn(backgroundScope) runCurrent() val updateFunc = (setting as DeviceSettingModel.MultiTogglePreference).updateState updateFunc(DeviceSettingStateModel.MultiTogglePreferenceState(2)) runCurrent() verify(settingProviderService1) verify(settingProviderService2) .updateDeviceSettings( DEVICE_INFO, DeviceSettingState.Builder() .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER) .setPreferenceState(ActionSwitchPreferenceState.Builder().build()) .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_ANC) .setPreferenceState( MultiTogglePreferenceState.Builder().setState(2).build() ) .build() ) } } private fun assertDeviceSetting(actual: DeviceSettingModel, serviceResponse: DeviceSetting) { assertThat(actual.id).isEqualTo(serviceResponse.settingId) when (actual) { is DeviceSettingModel.ActionSwitchPreference -> { assertThat(serviceResponse.preference) .isInstanceOf(ActionSwitchPreference::class.java) val pref = serviceResponse.preference as ActionSwitchPreference assertThat(actual.title).isEqualTo(pref.title) assertThat(actual.summary).isEqualTo(pref.summary) assertThat(actual.icon).isEqualTo(pref.icon) assertThat(actual.isAllowedChangingState).isEqualTo(pref.isAllowedChangingState) if (pref.hasSwitch()) { assertThat(actual.switchState!!.checked).isEqualTo(pref.checked) } else { assertThat(actual.switchState).isNull() } } is DeviceSettingModel.MultiTogglePreference -> { assertThat(serviceResponse.preference) .isInstanceOf(MultiTogglePreference::class.java) val pref = serviceResponse.preference as MultiTogglePreference assertThat(actual.title).isEqualTo(pref.title) assertThat(actual.isAllowedChangingState).isEqualTo(pref.isAllowedChangingState) assertThat(actual.toggles.size).isEqualTo(pref.toggleInfos.size) for (i in 0..<actual.toggles.size) { assertToggle(actual.toggles[i], pref.toggleInfos[i]) } } else -> {} } } private fun assertToggle(actual: ToggleModel, serviceResponse: ToggleInfo) { assertThat(actual.label).isEqualTo(serviceResponse.label) assertThat(actual.icon).isEqualTo(serviceResponse.icon) } private fun assertConfig( actual: DeviceSettingConfigModel, serviceResponse: DeviceSettingsConfig ) { assertThat(actual.mainItems.size).isEqualTo(serviceResponse.mainContentItems.size) for (i in 0..<actual.mainItems.size) { assertConfigItem(actual.mainItems[i], serviceResponse.mainContentItems[i]) } assertThat(actual.moreSettingsItems.size).isEqualTo(serviceResponse.moreSettingsItems.size) for (i in 0..<actual.moreSettingsItems.size) { assertConfigItem(actual.moreSettingsItems[i], serviceResponse.moreSettingsItems[i]) } assertThat(actual.moreSettingsPageFooter).isEqualTo(serviceResponse.moreSettingsFooter) } private fun assertConfigItem( actual: DeviceSettingConfigItemModel, serviceResponse: DeviceSettingItem ) { assertThat(actual.settingId).isEqualTo(serviceResponse.settingId) } private companion object { const val BLUETOOTH_ADDRESS = "12:34:56:78" const val CONFIG_SERVICE_PACKAGE_NAME = "com.android.fake.configservice" Loading Loading @@ -377,10 +460,21 @@ class DeviceSettingRepositoryTest { DeviceSetting.Builder() .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_ANC) .setPreference( ActionSwitchPreference.Builder() .setTitle("title2") .setHasSwitch(true) .setAllowedChangingState(true) MultiTogglePreference.Builder() .setTitle("title1") .setAllowChangingState(true) .addToggleInfo( ToggleInfo.Builder() .setLabel("label1") .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) .build() ) .addToggleInfo( ToggleInfo.Builder() .setLabel("label2") .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) .build() ) .build() ) .build() Loading Loading
packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt +98 −42 Original line number Diff line number Diff line Loading @@ -19,37 +19,39 @@ package com.android.settingslib.bluetooth.devicesettings.data.repository import android.bluetooth.BluetoothAdapter import android.content.Context import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreference import com.android.settingslib.bluetooth.devicesettings.DeviceSetting import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId import com.android.settingslib.bluetooth.devicesettings.DeviceSettingPreferenceState import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig import java.util.concurrent.ConcurrentHashMap import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference import com.android.settingslib.bluetooth.devicesettings.ToggleInfo 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.DeviceSettingModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel import com.google.common.cache.CacheBuilder import com.google.common.cache.CacheLoader import com.google.common.cache.LoadingCache import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch /** Provides functionality to control bluetooth device settings. */ interface DeviceSettingRepository { /** Gets config for the bluetooth device, returns null if failed. */ suspend fun getDeviceSettingsConfig(cachedDevice: CachedBluetoothDevice): DeviceSettingsConfig? /** Gets all device settings for the bluetooth device. */ fun getDeviceSettingList( cachedDevice: CachedBluetoothDevice, ): Flow<List<DeviceSetting>?> suspend fun getDeviceSettingsConfig( cachedDevice: CachedBluetoothDevice ): DeviceSettingConfigModel? /** Gets device setting for the bluetooth device. */ fun getDeviceSetting( cachedDevice: CachedBluetoothDevice, @DeviceSettingId settingId: Int ): Flow<DeviceSetting?> /** Updates device setting for the bluetooth device. */ suspend fun updateDeviceSettingState( cachedDevice: CachedBluetoothDevice, @DeviceSettingId deviceSettingId: Int, deviceSettingPreferenceState: DeviceSettingPreferenceState, ) ): Flow<DeviceSettingModel?> } class DeviceSettingRepositoryImpl( Loading @@ -58,40 +60,94 @@ class DeviceSettingRepositoryImpl( private val coroutineScope: CoroutineScope, private val backgroundCoroutineContext: CoroutineContext, ) : DeviceSettingRepository { private val deviceSettings = ConcurrentHashMap<CachedBluetoothDevice, DeviceSettingServiceConnection>() override suspend fun getDeviceSettingsConfig( private val connectionCache: LoadingCache<CachedBluetoothDevice, DeviceSettingServiceConnection> = CacheBuilder.newBuilder() .weakValues() .build( object : CacheLoader<CachedBluetoothDevice, DeviceSettingServiceConnection>() { override fun load( cachedDevice: CachedBluetoothDevice ): DeviceSettingsConfig? = createConnectionIfAbsent(cachedDevice).getDeviceSettingsConfig() ): DeviceSettingServiceConnection = DeviceSettingServiceConnection( cachedDevice, context, bluetoothAdaptor, coroutineScope, backgroundCoroutineContext, ) } ) override fun getDeviceSettingList( override suspend fun getDeviceSettingsConfig( cachedDevice: CachedBluetoothDevice ): Flow<List<DeviceSetting>?> = createConnectionIfAbsent(cachedDevice).getDeviceSettingList() ): DeviceSettingConfigModel? = connectionCache.get(cachedDevice).getDeviceSettingsConfig()?.toModel() override fun getDeviceSetting( cachedDevice: CachedBluetoothDevice, settingId: Int ): Flow<DeviceSetting?> = createConnectionIfAbsent(cachedDevice).getDeviceSetting(settingId) ): Flow<DeviceSettingModel?> = connectionCache.get(cachedDevice).let { connection -> connection.getDeviceSetting(settingId).map { it?.toModel(cachedDevice, connection) } } override suspend fun updateDeviceSettingState( cachedDevice: CachedBluetoothDevice, @DeviceSettingId deviceSettingId: Int, deviceSettingPreferenceState: DeviceSettingPreferenceState, ) = createConnectionIfAbsent(cachedDevice) .updateDeviceSettings(deviceSettingId, deviceSettingPreferenceState) private fun DeviceSettingsConfig.toModel(): DeviceSettingConfigModel = DeviceSettingConfigModel( mainItems = mainContentItems.map { it.toModel() }, moreSettingsItems = moreSettingsItems.map { it.toModel() }, moreSettingsPageFooter = moreSettingsFooter ) private fun createConnectionIfAbsent( cachedDevice: CachedBluetoothDevice ): DeviceSettingServiceConnection = deviceSettings.computeIfAbsent(cachedDevice) { DeviceSettingServiceConnection( cachedDevice, context, bluetoothAdaptor, coroutineScope, backgroundCoroutineContext, private fun DeviceSettingItem.toModel(): DeviceSettingConfigItemModel = DeviceSettingConfigItemModel(settingId) private fun DeviceSetting.toModel( cachedDevice: CachedBluetoothDevice, connection: DeviceSettingServiceConnection ): DeviceSettingModel = when (val pref = preference) { is ActionSwitchPreference -> DeviceSettingModel.ActionSwitchPreference( cachedDevice = cachedDevice, id = settingId, title = pref.title, summary = pref.summary, icon = pref.icon, isAllowedChangingState = pref.isAllowedChangingState, intent = pref.intent, switchState = if (pref.hasSwitch()) { DeviceSettingStateModel.ActionSwitchPreferenceState(pref.checked) } else { null }, updateState = { newState -> coroutineScope.launch(backgroundCoroutineContext) { connection.updateDeviceSettings( settingId, newState.toParcelable(), ) } }, ) is MultiTogglePreference -> DeviceSettingModel.MultiTogglePreference( cachedDevice = cachedDevice, id = settingId, title = pref.title, toggles = pref.toggleInfos.map { it.toModel() }, isAllowedChangingState = pref.isAllowedChangingState, isActive = true, state = DeviceSettingStateModel.MultiTogglePreferenceState(pref.state), updateState = { newState -> coroutineScope.launch(backgroundCoroutineContext) { connection.updateDeviceSettings(settingId, newState.toParcelable()) } }, ) else -> DeviceSettingModel.Unknown(cachedDevice, settingId) } private fun ToggleInfo.toModel(): ToggleModel = ToggleModel(label, icon) }
packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt 0 → 100644 +33 −0 Original line number Diff line number Diff line /* * 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.settingslib.bluetooth.devicesettings.shared.model import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId /** Models a device setting config. */ data class DeviceSettingConfigModel( /** Items need to be shown in device details main page. */ val mainItems: List<DeviceSettingConfigItemModel>, /** Items need to be shown in device details more settings page. */ val moreSettingsItems: List<DeviceSettingConfigItemModel>, /** Footer text in more settings page. */ val moreSettingsPageFooter: String) /** Models a device setting item in config. */ data class DeviceSettingConfigItemModel( @DeviceSettingId val settingId: Int, )
packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt +143 −49 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.graphics.Bitmap import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreference import com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreferenceState Loading @@ -34,6 +35,14 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreferenceState import com.android.settingslib.bluetooth.devicesettings.ToggleInfo 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.DeviceSettingModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay Loading Loading @@ -148,7 +157,7 @@ class DeviceSettingRepositoryTest { val config = underTest.getDeviceSettingsConfig(cachedDevice) assertThat(config).isSameInstanceAs(DEVICE_SETTING_CONFIG) assertConfig(config!!, DEVICE_SETTING_CONFIG) } } Loading @@ -163,7 +172,7 @@ class DeviceSettingRepositoryTest { ) .thenReturn("".toByteArray()) var config: DeviceSettingsConfig? = null var config: DeviceSettingConfigModel? = null val job = launch { config = underTest.getDeviceSettingsConfig(cachedDevice) } delay(1000) verify(bluetoothAdapter) Loading @@ -185,7 +194,7 @@ class DeviceSettingRepositoryTest { .thenReturn(BLUETOOTH_DEVICE_METADATA.toByteArray()) job.join() assertThat(config).isSameInstanceAs(DEVICE_SETTING_CONFIG) assertConfig(config!!, DEVICE_SETTING_CONFIG) } } Loading @@ -202,7 +211,7 @@ class DeviceSettingRepositoryTest { } @Test fun getDeviceSettingList_success() { fun getDeviceSetting_actionSwitchPreference_success() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { Loading @@ -211,64 +220,63 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) } var setting: DeviceSettingModel? = null underTest .getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_HEADER) .onEach { setting = it } .launchIn(backgroundScope) runCurrent() assertDeviceSetting(setting!!, DEVICE_SETTING_1) } } @Test fun getDeviceSetting_multiTogglePreference_success() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then { input -> input .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_2)) } var settings: List<DeviceSetting>? = null var setting: DeviceSettingModel? = null underTest .getDeviceSettingList(cachedDevice) .onEach { settings = it } .getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC) .onEach { setting = it } .launchIn(backgroundScope) runCurrent() assertThat(settings?.map { it.settingId }) .containsExactly( DeviceSettingId.DEVICE_SETTING_ID_HEADER, DeviceSettingId.DEVICE_SETTING_ID_ANC ) assertThat(settings?.map { (it.preference as ActionSwitchPreference).title }) .containsExactly( "title1", "title2", ) assertDeviceSetting(setting!!, DEVICE_SETTING_2) } } @Test fun getDeviceSetting_oneServiceFailed_returnPartialResult() { fun getDeviceSetting_noConfig_returnNull() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { input -> input .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) } var settings: List<DeviceSetting>? = null var setting: DeviceSettingModel? = null underTest .getDeviceSettingList(cachedDevice) .onEach { settings = it } .getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_HEADER) .onEach { setting = it } .launchIn(backgroundScope) runCurrent() assertThat(settings?.map { it.settingId }) .containsExactly( DeviceSettingId.DEVICE_SETTING_ID_HEADER, ) assertThat(settings?.map { (it.preference as ActionSwitchPreference).title }) .containsExactly( "title1", ) assertThat(setting).isNull() } } @Test fun getDeviceSetting_success() { fun updateDeviceSettingState_switchState_success() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { Loading @@ -277,48 +285,123 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) } var setting: DeviceSetting? = null var setting: DeviceSettingModel? = null underTest .getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_HEADER) .onEach { setting = it } .launchIn(backgroundScope) runCurrent() val updateFunc = (setting as DeviceSettingModel.ActionSwitchPreference).updateState!! updateFunc(DeviceSettingStateModel.ActionSwitchPreferenceState(false)) runCurrent() assertThat(setting?.settingId).isEqualTo(DeviceSettingId.DEVICE_SETTING_ID_HEADER) assertThat((setting?.preference as ActionSwitchPreference).title).isEqualTo("title1") verify(settingProviderService1) .updateDeviceSettings( DEVICE_INFO, DeviceSettingState.Builder() .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER) .setPreferenceState( ActionSwitchPreferenceState.Builder().setChecked(false).build() ) .build() ) } } @Test fun updateDeviceSetting_success() { fun updateDeviceSettingState_multiToggleState_success() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then { input -> input .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_2)) } var setting: DeviceSettingModel? = null underTest.updateDeviceSettingState( cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_HEADER, ActionSwitchPreferenceState.Builder().build() ) underTest .getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC) .onEach { setting = it } .launchIn(backgroundScope) runCurrent() val updateFunc = (setting as DeviceSettingModel.MultiTogglePreference).updateState updateFunc(DeviceSettingStateModel.MultiTogglePreferenceState(2)) runCurrent() verify(settingProviderService1) verify(settingProviderService2) .updateDeviceSettings( DEVICE_INFO, DeviceSettingState.Builder() .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER) .setPreferenceState(ActionSwitchPreferenceState.Builder().build()) .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_ANC) .setPreferenceState( MultiTogglePreferenceState.Builder().setState(2).build() ) .build() ) } } private fun assertDeviceSetting(actual: DeviceSettingModel, serviceResponse: DeviceSetting) { assertThat(actual.id).isEqualTo(serviceResponse.settingId) when (actual) { is DeviceSettingModel.ActionSwitchPreference -> { assertThat(serviceResponse.preference) .isInstanceOf(ActionSwitchPreference::class.java) val pref = serviceResponse.preference as ActionSwitchPreference assertThat(actual.title).isEqualTo(pref.title) assertThat(actual.summary).isEqualTo(pref.summary) assertThat(actual.icon).isEqualTo(pref.icon) assertThat(actual.isAllowedChangingState).isEqualTo(pref.isAllowedChangingState) if (pref.hasSwitch()) { assertThat(actual.switchState!!.checked).isEqualTo(pref.checked) } else { assertThat(actual.switchState).isNull() } } is DeviceSettingModel.MultiTogglePreference -> { assertThat(serviceResponse.preference) .isInstanceOf(MultiTogglePreference::class.java) val pref = serviceResponse.preference as MultiTogglePreference assertThat(actual.title).isEqualTo(pref.title) assertThat(actual.isAllowedChangingState).isEqualTo(pref.isAllowedChangingState) assertThat(actual.toggles.size).isEqualTo(pref.toggleInfos.size) for (i in 0..<actual.toggles.size) { assertToggle(actual.toggles[i], pref.toggleInfos[i]) } } else -> {} } } private fun assertToggle(actual: ToggleModel, serviceResponse: ToggleInfo) { assertThat(actual.label).isEqualTo(serviceResponse.label) assertThat(actual.icon).isEqualTo(serviceResponse.icon) } private fun assertConfig( actual: DeviceSettingConfigModel, serviceResponse: DeviceSettingsConfig ) { assertThat(actual.mainItems.size).isEqualTo(serviceResponse.mainContentItems.size) for (i in 0..<actual.mainItems.size) { assertConfigItem(actual.mainItems[i], serviceResponse.mainContentItems[i]) } assertThat(actual.moreSettingsItems.size).isEqualTo(serviceResponse.moreSettingsItems.size) for (i in 0..<actual.moreSettingsItems.size) { assertConfigItem(actual.moreSettingsItems[i], serviceResponse.moreSettingsItems[i]) } assertThat(actual.moreSettingsPageFooter).isEqualTo(serviceResponse.moreSettingsFooter) } private fun assertConfigItem( actual: DeviceSettingConfigItemModel, serviceResponse: DeviceSettingItem ) { assertThat(actual.settingId).isEqualTo(serviceResponse.settingId) } private companion object { const val BLUETOOTH_ADDRESS = "12:34:56:78" const val CONFIG_SERVICE_PACKAGE_NAME = "com.android.fake.configservice" Loading Loading @@ -377,10 +460,21 @@ class DeviceSettingRepositoryTest { DeviceSetting.Builder() .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_ANC) .setPreference( ActionSwitchPreference.Builder() .setTitle("title2") .setHasSwitch(true) .setAllowedChangingState(true) MultiTogglePreference.Builder() .setTitle("title1") .setAllowChangingState(true) .addToggleInfo( ToggleInfo.Builder() .setLabel("label1") .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) .build() ) .addToggleInfo( ToggleInfo.Builder() .setLabel("label2") .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) .build() ) .build() ) .build() Loading