Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b3504b83 authored by Haijie Hong's avatar Haijie Hong Committed by Android (Google) Code Review
Browse files

Merge "Refactor data layer of device details page" into main

parents 1b303278 04a4f48f
Loading
Loading
Loading
Loading
+98 −42
Original line number Diff line number Diff line
@@ -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(
@@ -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)
}
+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,
)
+143 −49
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -148,7 +157,7 @@ class DeviceSettingRepositoryTest {

            val config = underTest.getDeviceSettingsConfig(cachedDevice)

            assertThat(config).isSameInstanceAs(DEVICE_SETTING_CONFIG)
            assertConfig(config!!, DEVICE_SETTING_CONFIG)
        }
    }

@@ -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)
@@ -185,7 +194,7 @@ class DeviceSettingRepositoryTest {
                .thenReturn(BLUETOOTH_DEVICE_METADATA.toByteArray())

            job.join()
            assertThat(config).isSameInstanceAs(DEVICE_SETTING_CONFIG)
            assertConfig(config!!, DEVICE_SETTING_CONFIG)
        }
    }

@@ -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 {
@@ -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 {
@@ -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"
@@ -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()