diff options
7 files changed, 354 insertions, 126 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.aidl new file mode 100644 index 000000000000..1726036f0ded --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.aidl @@ -0,0 +1,19 @@ +/* + * 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; + +parcelable DeviceSettingsProviderServiceStatus;
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt new file mode 100644 index 000000000000..977849e75556 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt @@ -0,0 +1,60 @@ +/* + * 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 + +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable + +/** + * A data class representing a device settings item in bluetooth device details config. + * + * @property enabled Whether the service is enabled. + * @property extras Extra bundle + */ +data class DeviceSettingsProviderServiceStatus( + val enabled: Boolean, + val extras: Bundle = Bundle.EMPTY, +) : Parcelable { + + override fun describeContents(): Int = 0 + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.run { + writeBoolean(enabled) + writeBundle(extras) + } + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator<DeviceSettingsProviderServiceStatus> = + object : Parcelable.Creator<DeviceSettingsProviderServiceStatus> { + override fun createFromParcel(parcel: Parcel) = + parcel.run { + DeviceSettingsProviderServiceStatus( + enabled = readBoolean(), + extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY, + ) + } + + override fun newArray(size: Int): Array<DeviceSettingsProviderServiceStatus?> { + return arrayOfNulls(size) + } + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl index d5efac9d0336..1c0a1fd6b798 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl @@ -18,10 +18,12 @@ package com.android.settingslib.bluetooth.devicesettings; import com.android.settingslib.bluetooth.devicesettings.DeviceInfo; import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState; +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsProviderServiceStatus; import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener; -oneway interface IDeviceSettingsProviderService { - void registerDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); - void unregisterDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); - void updateDeviceSettings(in DeviceInfo device, in DeviceSettingState params); +interface IDeviceSettingsProviderService { + DeviceSettingsProviderServiceStatus getServiceStatus(); + oneway void registerDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); + oneway void unregisterDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); + oneway void updateDeviceSettings(in DeviceInfo device, in DeviceSettingState params); }
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/model/ServiceConnectionStatus.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/model/ServiceConnectionStatus.kt new file mode 100644 index 000000000000..25080bcef061 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/model/ServiceConnectionStatus.kt @@ -0,0 +1,31 @@ +/* + * 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.data.model + +import android.os.IInterface + +/** Present a service connection status. */ +sealed interface ServiceConnectionStatus<out T : IInterface> { + /** Service is connecting. */ + data object Connecting : ServiceConnectionStatus<Nothing> + + /** Service is connected. */ + data class Connected<T : IInterface>(val service: T) : ServiceConnectionStatus<T> + + /** Service connection failed. */ + data object Failed : ServiceConnectionStatus<Nothing> +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt index d6b28629d16b..33beb06e2ed5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt @@ -22,7 +22,8 @@ import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.IBinder -import com.android.internal.util.ConcurrentUtils +import android.os.IInterface +import android.util.Log import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.devicesettings.DeviceInfo @@ -34,27 +35,28 @@ 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.data.model.ServiceConnectionStatus import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicReference import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -84,64 +86,132 @@ class DeviceSettingServiceConnection( } } - private var config = AtomicReference<DeviceSettingsConfig?>(null) - private var idToSetting = AtomicReference<Flow<Map<Int, DeviceSetting>>?>(null) + private var isServiceEnabled = + coroutineScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) { + val states = getSettingsProviderServices()?.values ?: return@async false + combine(states) { it.toList() } + .mapNotNull { allStatus -> + if (allStatus.any { it is ServiceConnectionStatus.Failed }) { + false + } else if (allStatus.all { it is ServiceConnectionStatus.Connected }) { + allStatus + .filterIsInstance< + ServiceConnectionStatus.Connected<IDeviceSettingsProviderService> + >() + .all { it.service.serviceStatus?.enabled == true } + } else { + null + } + } + .first() + } - /** Gets [DeviceSettingsConfig] for the device, return null when failed. */ - suspend fun getDeviceSettingsConfig(): DeviceSettingsConfig? = - config.computeIfAbsent { - getConfigServiceBindingIntent(cachedDevice) - .flatMapLatest { getService(it) } - .map { it?.let { IDeviceSettingsConfigProviderService.Stub.asInterface(it) } } - .map { - it?.getDeviceSettingsConfig( - deviceInfo { setBluetoothAddress(cachedDevice.address) } - ) + private var config = + coroutineScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) { + val intent = + tryGetEndpointFromMetadata(cachedDevice)?.toIntent() + ?: run { + Log.i(TAG, "Unable to read device setting metadata from $cachedDevice") + return@async null + } + getService(intent, IDeviceSettingsConfigProviderService.Stub::asInterface) + .flatMapConcat { + when (it) { + is ServiceConnectionStatus.Connected -> + flowOf( + it.service.getDeviceSettingsConfig( + deviceInfo { setBluetoothAddress(cachedDevice.address) } + ) + ) + ServiceConnectionStatus.Connecting -> flowOf() + ServiceConnectionStatus.Failed -> flowOf(null) + } } .first() } + private val settingIdToItemMapping = + flow { + if (!isServiceEnabled.await()) { + Log.w(TAG, "Service is disabled") + return@flow + } + getSettingsProviderServices() + ?.values + ?.map { + it.flatMapLatest { status -> + when (status) { + is ServiceConnectionStatus.Connected -> + getDeviceSettingsFromService(cachedDevice, status.service) + else -> flowOf(emptyList()) + } + } + } + ?.let { items -> combine(items) { it.toList().flatten() } } + ?.map { items -> items.associateBy { it.settingId } } + ?.let { emitAll(it) } + } + .shareIn(scope = coroutineScope, started = SharingStarted.WhileSubscribed(), replay = 1) + + /** Gets [DeviceSettingsConfig] for the device, return null when failed. */ + suspend fun getDeviceSettingsConfig(): DeviceSettingsConfig? { + if (!isServiceEnabled.await()) { + Log.w(TAG, "Service is disabled") + return null + } + return readConfig() + } + /** Gets all device settings for the device. */ fun getDeviceSettingList(): Flow<List<DeviceSetting>> = - getSettingIdToItemMapping().map { it.values.toList() } + settingIdToItemMapping.map { it.values.toList() } /** Gets the device settings with the ID for the device. */ fun getDeviceSetting(@DeviceSettingId deviceSettingId: Int): Flow<DeviceSetting?> = - getSettingIdToItemMapping().map { it[deviceSettingId] } + settingIdToItemMapping.map { it[deviceSettingId] } /** Updates the device setting state for the device. */ suspend fun updateDeviceSettings( @DeviceSettingId deviceSettingId: Int, deviceSettingPreferenceState: DeviceSettingPreferenceState, ) { - getDeviceSettingsConfig()?.let { config -> + if (!isServiceEnabled.await()) { + Log.w(TAG, "Service is disabled") + return + } + readConfig()?.let { config -> (config.mainContentItems + config.moreSettingsItems) .find { it.settingId == deviceSettingId } ?.let { getSettingsProviderServices() ?.get(EndPoint(it.packageName, it.className, it.intentAction)) - ?.filterNotNull() + ?.filterIsInstance< + ServiceConnectionStatus.Connected<IDeviceSettingsProviderService> + >() ?.first() } + ?.service ?.updateDeviceSettings( deviceInfo { setBluetoothAddress(cachedDevice.address) }, DeviceSettingState.Builder() .setSettingId(deviceSettingId) .setPreferenceState(deviceSettingPreferenceState) - .build() + .build(), ) } } + private suspend fun readConfig(): DeviceSettingsConfig? = config.await() + private suspend fun getSettingsProviderServices(): - Map<EndPoint, StateFlow<IDeviceSettingsProviderService?>>? = - getDeviceSettingsConfig() + Map<EndPoint, StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>>? = + readConfig() ?.let { config -> (config.mainContentItems + config.moreSettingsItems).map { EndPoint( packageName = it.packageName, className = it.className, - intentAction = it.intentAction + intentAction = it.intentAction, ) } } @@ -150,43 +220,22 @@ class DeviceSettingServiceConnection( { it }, { endpoint -> services.computeIfAbsent(endpoint) { - getService(endpoint.toIntent()) - .map { service -> - IDeviceSettingsProviderService.Stub.asInterface(service) - } - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null) + getService( + endpoint.toIntent(), + IDeviceSettingsProviderService.Stub::asInterface, + ) + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(), + ServiceConnectionStatus.Connecting, + ) } - } + }, ) - private fun getSettingIdToItemMapping(): Flow<Map<Int, DeviceSetting>> = - idToSetting.computeIfAbsent { - flow { - getSettingsProviderServices() - ?.values - ?.map { - it.flatMapLatest { service -> - if (service != null) { - getDeviceSettingsFromService(cachedDevice, service) - } else { - flowOf(emptyList()) - } - } - } - ?.let { items -> combine(items) { it.toList().flatten() } } - ?.map { items -> items.associateBy { it.settingId } } - ?.let { emitAll(it) } - } - .shareIn( - scope = coroutineScope, - started = SharingStarted.WhileSubscribed(), - replay = 1 - ) - }!! - private fun getDeviceSettingsFromService( cachedDevice: CachedBluetoothDevice, - service: IDeviceSettingsProviderService + service: IDeviceSettingsProviderService, ): Flow<List<DeviceSetting>> { return callbackFlow { val listener = @@ -202,51 +251,28 @@ class DeviceSettingServiceConnection( .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) } - private fun getService(intent: Intent): Flow<IBinder?> { + private fun <T : IInterface> getService( + intent: Intent, + transform: ((IBinder) -> T), + ): Flow<ServiceConnectionStatus<T>> { return callbackFlow { val serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { - launch { send(service) } + launch { send(ServiceConnectionStatus.Connected(transform(service))) } } override fun onServiceDisconnected(name: ComponentName?) { - launch { send(null) } + launch { send(ServiceConnectionStatus.Connecting) } } } if (!context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)) { - launch { send(null) } + launch { send(ServiceConnectionStatus.Failed) } } awaitClose { context.unbindService(serviceConnection) } } } - private fun getConfigServiceBindingIntent(cachedDevice: CachedBluetoothDevice): Flow<Intent> { - return callbackFlow { - val listener = - BluetoothAdapter.OnMetadataChangedListener { device, key, _ -> - if ( - key == METADATA_FAST_PAIR_CUSTOMIZED_FIELDS && - cachedDevice.device == device - ) { - launch { tryGetEndpointFromMetadata(cachedDevice)?.let { send(it) } } - } - } - bluetoothAdaptor.addOnMetadataChangedListener( - cachedDevice.device, - ConcurrentUtils.DIRECT_EXECUTOR, - listener, - ) - awaitClose { - bluetoothAdaptor.removeOnMetadataChangedListener(cachedDevice.device, listener) - } - } - .onStart { tryGetEndpointFromMetadata(cachedDevice)?.let { emit(it) } } - .distinctUntilChanged() - .map { it.toIntent() } - .flowOn(backgroundCoroutineContext) - } - private suspend fun tryGetEndpointFromMetadata(cachedDevice: CachedBluetoothDevice): EndPoint? = withContext(backgroundCoroutineContext) { val packageName = @@ -257,29 +283,31 @@ class DeviceSettingServiceConnection( val className = BluetoothUtils.getFastPairCustomizedField( cachedDevice.device, - CONFIG_SERVICE_CLASS_NAME + CONFIG_SERVICE_CLASS_NAME, ) ?: return@withContext null val intentAction = BluetoothUtils.getFastPairCustomizedField( cachedDevice.device, - CONFIG_SERVICE_INTENT_ACTION + CONFIG_SERVICE_INTENT_ACTION, ) ?: return@withContext null EndPoint(packageName, className, intentAction) } - private inline fun <T> AtomicReference<T?>.computeIfAbsent(producer: () -> T): T? = - get() ?: producer().let { compareAndExchange(null, it) ?: it } - private inline fun deviceInfo(block: DeviceInfo.Builder.() -> Unit): DeviceInfo { return DeviceInfo.Builder().apply { block() }.build() } companion object { + const val TAG = "DeviceSettingSrvConn" const val METADATA_FAST_PAIR_CUSTOMIZED_FIELDS: Int = 25 const val CONFIG_SERVICE_PACKAGE_NAME = "DEVICE_SETTINGS_CONFIG_PACKAGE_NAME" const val CONFIG_SERVICE_CLASS_NAME = "DEVICE_SETTINGS_CONFIG_CLASS" const val CONFIG_SERVICE_INTENT_ACTION = "DEVICE_SETTINGS_CONFIG_ACTION" - val services = ConcurrentHashMap<EndPoint, StateFlow<IDeviceSettingsProviderService?>>() + val services = + ConcurrentHashMap< + EndPoint, + StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>, + >() } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatusTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatusTest.kt new file mode 100644 index 000000000000..aa22fac49cd8 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatusTest.kt @@ -0,0 +1,51 @@ +/* + * 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 + +import android.os.Bundle +import android.os.Parcel +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DeviceSettingsProviderServiceStatusTest { + + @Test + fun parcelOperation() { + val item = + DeviceSettingsProviderServiceStatus( + enabled = true, + extras = Bundle().apply { putString("key1", "value1") }, + ) + + val fromParcel = writeAndRead(item) + + assertThat(fromParcel.enabled).isEqualTo(item.enabled) + assertThat(fromParcel.extras.getString("key1")).isEqualTo(item.extras.getString("key1")) + } + + private fun writeAndRead( + item: DeviceSettingsProviderServiceStatus + ): DeviceSettingsProviderServiceStatus { + val parcel = Parcel.obtain() + item.writeToParcel(parcel, 0) + parcel.setDataPosition(0) + return DeviceSettingsProviderServiceStatus.CREATOR.createFromParcel(parcel) + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt index 95ee46e4fdb9..ce155b5c0fa4 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt @@ -33,6 +33,7 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsProviderServiceStatus import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService @@ -47,10 +48,8 @@ import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSetti import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -59,12 +58,9 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString -import org.mockito.ArgumentMatchers.eq -import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.doReturn import org.mockito.Mockito.verify @@ -85,9 +81,6 @@ class DeviceSettingRepositoryTest { @Mock private lateinit var configService: IDeviceSettingsConfigProviderService.Stub @Mock private lateinit var settingProviderService1: IDeviceSettingsProviderService.Stub @Mock private lateinit var settingProviderService2: IDeviceSettingsProviderService.Stub - @Captor - private lateinit var metadataChangeCaptor: - ArgumentCaptor<BluetoothAdapter.OnMetadataChangedListener> private lateinit var underTest: DeviceSettingRepository private val testScope = TestScope() @@ -153,6 +146,12 @@ class DeviceSettingRepositoryTest { fun getDeviceSettingsConfig_withMetadata_success() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) val config = underTest.getDeviceSettingsConfig(cachedDevice) @@ -161,32 +160,40 @@ class DeviceSettingRepositoryTest { } @Test - fun getDeviceSettingsConfig_waitMetadataChange_success() { + fun getDeviceSettingsConfig_noMetadata_returnNull() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`( - bluetoothDevice.getMetadata( - DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + bluetoothDevice.getMetadata( + DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) .thenReturn("".toByteArray()) + `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) - var config: DeviceSettingConfigModel? = null - val job = launch { config = underTest.getDeviceSettingsConfig(cachedDevice) } - delay(1000) - verify(bluetoothAdapter) - .addOnMetadataChangedListener( - eq(bluetoothDevice), any(), metadataChangeCaptor.capture()) - metadataChangeCaptor.value.onMetadataChanged( - bluetoothDevice, - DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, - BLUETOOTH_DEVICE_METADATA.toByteArray(), + val config = underTest.getDeviceSettingsConfig(cachedDevice) + + assertThat(config).isNull() + } + } + + @Test + fun getDeviceSettingsConfig_providerServiceNotEnabled_returnNull() { + testScope.runTest { + `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(false) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) ) - `when`( - bluetoothDevice.getMetadata( - DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) - .thenReturn(BLUETOOTH_DEVICE_METADATA.toByteArray()) - job.join() - assertConfig(config!!, DEVICE_SETTING_CONFIG) + val config = underTest.getDeviceSettingsConfig(cachedDevice) + + assertThat(config).isNull() } } @@ -212,6 +219,12 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) } + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) var setting: DeviceSettingModel? = null underTest @@ -234,6 +247,12 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_2)) } + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) var setting: DeviceSettingModel? = null underTest @@ -256,6 +275,12 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_HELP)) } + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) var setting: DeviceSettingModel? = null underTest @@ -299,6 +324,12 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) } + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) var setting: DeviceSettingModel? = null underTest @@ -331,6 +362,12 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_2)) } + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) var setting: DeviceSettingModel? = null underTest |