Loading packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.aidl 0 → 100644 +19 −0 Original line number Original line 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; parcelable DeviceSettingsProviderServiceStatus; No newline at end of file packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt 0 → 100644 +60 −0 Original line number Original line 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 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) } } } } packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl +6 −4 Original line number Original line Diff line number Diff line Loading @@ -18,10 +18,12 @@ package com.android.settingslib.bluetooth.devicesettings; import com.android.settingslib.bluetooth.devicesettings.DeviceInfo; import com.android.settingslib.bluetooth.devicesettings.DeviceInfo; import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState; import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState; import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsProviderServiceStatus; import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener; import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener; oneway interface IDeviceSettingsProviderService { interface IDeviceSettingsProviderService { void registerDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); DeviceSettingsProviderServiceStatus getServiceStatus(); void unregisterDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); oneway void registerDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); void updateDeviceSettings(in DeviceInfo device, in DeviceSettingState params); oneway void unregisterDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); oneway void updateDeviceSettings(in DeviceInfo device, in DeviceSettingState params); } } No newline at end of file packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/model/ServiceConnectionStatus.kt 0 → 100644 +31 −0 Original line number Original line 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.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> } packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt +122 −94 Original line number Original line Diff line number Diff line Loading @@ -22,7 +22,8 @@ import android.content.Context import android.content.Intent import android.content.Intent import android.content.ServiceConnection import android.content.ServiceConnection import android.os.IBinder 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.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.devicesettings.DeviceInfo import com.android.settingslib.bluetooth.devicesettings.DeviceInfo Loading @@ -34,27 +35,28 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService 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.ConcurrentHashMap import java.util.concurrent.atomic.AtomicReference import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.launch Loading Loading @@ -84,64 +86,132 @@ class DeviceSettingServiceConnection( } } } } private var config = AtomicReference<DeviceSettingsConfig?>(null) private var isServiceEnabled = private var idToSetting = AtomicReference<Flow<Map<Int, DeviceSetting>>?>(null) 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. */ private var config = suspend fun getDeviceSettingsConfig(): DeviceSettingsConfig? = coroutineScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) { config.computeIfAbsent { val intent = getConfigServiceBindingIntent(cachedDevice) tryGetEndpointFromMetadata(cachedDevice)?.toIntent() .flatMapLatest { getService(it) } ?: run { .map { it?.let { IDeviceSettingsConfigProviderService.Stub.asInterface(it) } } Log.i(TAG, "Unable to read device setting metadata from $cachedDevice") .map { return@async null it?.getDeviceSettingsConfig( } getService(intent, IDeviceSettingsConfigProviderService.Stub::asInterface) .flatMapConcat { when (it) { is ServiceConnectionStatus.Connected -> flowOf( it.service.getDeviceSettingsConfig( deviceInfo { setBluetoothAddress(cachedDevice.address) } deviceInfo { setBluetoothAddress(cachedDevice.address) } ) ) ) ServiceConnectionStatus.Connecting -> flowOf() ServiceConnectionStatus.Failed -> flowOf(null) } } } .first() .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. */ /** Gets all device settings for the device. */ fun getDeviceSettingList(): Flow<List<DeviceSetting>> = 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. */ /** Gets the device settings with the ID for the device. */ fun getDeviceSetting(@DeviceSettingId deviceSettingId: Int): Flow<DeviceSetting?> = fun getDeviceSetting(@DeviceSettingId deviceSettingId: Int): Flow<DeviceSetting?> = getSettingIdToItemMapping().map { it[deviceSettingId] } settingIdToItemMapping.map { it[deviceSettingId] } /** Updates the device setting state for the device. */ /** Updates the device setting state for the device. */ suspend fun updateDeviceSettings( suspend fun updateDeviceSettings( @DeviceSettingId deviceSettingId: Int, @DeviceSettingId deviceSettingId: Int, deviceSettingPreferenceState: DeviceSettingPreferenceState, deviceSettingPreferenceState: DeviceSettingPreferenceState, ) { ) { getDeviceSettingsConfig()?.let { config -> if (!isServiceEnabled.await()) { Log.w(TAG, "Service is disabled") return } readConfig()?.let { config -> (config.mainContentItems + config.moreSettingsItems) (config.mainContentItems + config.moreSettingsItems) .find { it.settingId == deviceSettingId } .find { it.settingId == deviceSettingId } ?.let { ?.let { getSettingsProviderServices() getSettingsProviderServices() ?.get(EndPoint(it.packageName, it.className, it.intentAction)) ?.get(EndPoint(it.packageName, it.className, it.intentAction)) ?.filterNotNull() ?.filterIsInstance< ServiceConnectionStatus.Connected<IDeviceSettingsProviderService> >() ?.first() ?.first() } } ?.service ?.updateDeviceSettings( ?.updateDeviceSettings( deviceInfo { setBluetoothAddress(cachedDevice.address) }, deviceInfo { setBluetoothAddress(cachedDevice.address) }, DeviceSettingState.Builder() DeviceSettingState.Builder() .setSettingId(deviceSettingId) .setSettingId(deviceSettingId) .setPreferenceState(deviceSettingPreferenceState) .setPreferenceState(deviceSettingPreferenceState) .build() .build(), ) ) } } } } private suspend fun readConfig(): DeviceSettingsConfig? = config.await() private suspend fun getSettingsProviderServices(): private suspend fun getSettingsProviderServices(): Map<EndPoint, StateFlow<IDeviceSettingsProviderService?>>? = Map<EndPoint, StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>>? = getDeviceSettingsConfig() readConfig() ?.let { config -> ?.let { config -> (config.mainContentItems + config.moreSettingsItems).map { (config.mainContentItems + config.moreSettingsItems).map { EndPoint( EndPoint( packageName = it.packageName, packageName = it.packageName, className = it.className, className = it.className, intentAction = it.intentAction intentAction = it.intentAction, ) ) } } } } Loading @@ -150,43 +220,22 @@ class DeviceSettingServiceConnection( { it }, { it }, { endpoint -> { endpoint -> services.computeIfAbsent(endpoint) { services.computeIfAbsent(endpoint) { getService(endpoint.toIntent()) getService( .map { service -> endpoint.toIntent(), IDeviceSettingsProviderService.Stub.asInterface(service) IDeviceSettingsProviderService.Stub::asInterface, } ) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null) .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( private fun getDeviceSettingsFromService( cachedDevice: CachedBluetoothDevice, cachedDevice: CachedBluetoothDevice, service: IDeviceSettingsProviderService service: IDeviceSettingsProviderService, ): Flow<List<DeviceSetting>> { ): Flow<List<DeviceSetting>> { return callbackFlow { return callbackFlow { val listener = val listener = Loading @@ -202,51 +251,28 @@ class DeviceSettingServiceConnection( .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) .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 { return callbackFlow { val serviceConnection = val serviceConnection = object : ServiceConnection { object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { override fun onServiceConnected(name: ComponentName, service: IBinder) { launch { send(service) } launch { send(ServiceConnectionStatus.Connected(transform(service))) } } } override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) { launch { send(null) } launch { send(ServiceConnectionStatus.Connecting) } } } } } if (!context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)) { if (!context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)) { launch { send(null) } launch { send(ServiceConnectionStatus.Failed) } } } awaitClose { context.unbindService(serviceConnection) } 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? = private suspend fun tryGetEndpointFromMetadata(cachedDevice: CachedBluetoothDevice): EndPoint? = withContext(backgroundCoroutineContext) { withContext(backgroundCoroutineContext) { val packageName = val packageName = Loading @@ -257,29 +283,31 @@ class DeviceSettingServiceConnection( val className = val className = BluetoothUtils.getFastPairCustomizedField( BluetoothUtils.getFastPairCustomizedField( cachedDevice.device, cachedDevice.device, CONFIG_SERVICE_CLASS_NAME CONFIG_SERVICE_CLASS_NAME, ) ?: return@withContext null ) ?: return@withContext null val intentAction = val intentAction = BluetoothUtils.getFastPairCustomizedField( BluetoothUtils.getFastPairCustomizedField( cachedDevice.device, cachedDevice.device, CONFIG_SERVICE_INTENT_ACTION CONFIG_SERVICE_INTENT_ACTION, ) ?: return@withContext null ) ?: return@withContext null EndPoint(packageName, className, intentAction) 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 { private inline fun deviceInfo(block: DeviceInfo.Builder.() -> Unit): DeviceInfo { return DeviceInfo.Builder().apply { block() }.build() return DeviceInfo.Builder().apply { block() }.build() } } companion object { companion object { const val TAG = "DeviceSettingSrvConn" const val METADATA_FAST_PAIR_CUSTOMIZED_FIELDS: Int = 25 const val METADATA_FAST_PAIR_CUSTOMIZED_FIELDS: Int = 25 const val CONFIG_SERVICE_PACKAGE_NAME = "DEVICE_SETTINGS_CONFIG_PACKAGE_NAME" 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_CLASS_NAME = "DEVICE_SETTINGS_CONFIG_CLASS" const val CONFIG_SERVICE_INTENT_ACTION = "DEVICE_SETTINGS_CONFIG_ACTION" const val CONFIG_SERVICE_INTENT_ACTION = "DEVICE_SETTINGS_CONFIG_ACTION" val services = ConcurrentHashMap<EndPoint, StateFlow<IDeviceSettingsProviderService?>>() val services = ConcurrentHashMap< EndPoint, StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>, >() } } } } Loading
packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.aidl 0 → 100644 +19 −0 Original line number Original line 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; parcelable DeviceSettingsProviderServiceStatus; No newline at end of file
packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt 0 → 100644 +60 −0 Original line number Original line 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 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) } } } }
packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl +6 −4 Original line number Original line Diff line number Diff line Loading @@ -18,10 +18,12 @@ package com.android.settingslib.bluetooth.devicesettings; import com.android.settingslib.bluetooth.devicesettings.DeviceInfo; import com.android.settingslib.bluetooth.devicesettings.DeviceInfo; import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState; import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState; import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsProviderServiceStatus; import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener; import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener; oneway interface IDeviceSettingsProviderService { interface IDeviceSettingsProviderService { void registerDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); DeviceSettingsProviderServiceStatus getServiceStatus(); void unregisterDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); oneway void registerDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); void updateDeviceSettings(in DeviceInfo device, in DeviceSettingState params); oneway void unregisterDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); oneway void updateDeviceSettings(in DeviceInfo device, in DeviceSettingState params); } } No newline at end of file
packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/model/ServiceConnectionStatus.kt 0 → 100644 +31 −0 Original line number Original line 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.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> }
packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt +122 −94 Original line number Original line Diff line number Diff line Loading @@ -22,7 +22,8 @@ import android.content.Context import android.content.Intent import android.content.Intent import android.content.ServiceConnection import android.content.ServiceConnection import android.os.IBinder 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.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.devicesettings.DeviceInfo import com.android.settingslib.bluetooth.devicesettings.DeviceInfo Loading @@ -34,27 +35,28 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService 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.ConcurrentHashMap import java.util.concurrent.atomic.AtomicReference import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.launch Loading Loading @@ -84,64 +86,132 @@ class DeviceSettingServiceConnection( } } } } private var config = AtomicReference<DeviceSettingsConfig?>(null) private var isServiceEnabled = private var idToSetting = AtomicReference<Flow<Map<Int, DeviceSetting>>?>(null) 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. */ private var config = suspend fun getDeviceSettingsConfig(): DeviceSettingsConfig? = coroutineScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) { config.computeIfAbsent { val intent = getConfigServiceBindingIntent(cachedDevice) tryGetEndpointFromMetadata(cachedDevice)?.toIntent() .flatMapLatest { getService(it) } ?: run { .map { it?.let { IDeviceSettingsConfigProviderService.Stub.asInterface(it) } } Log.i(TAG, "Unable to read device setting metadata from $cachedDevice") .map { return@async null it?.getDeviceSettingsConfig( } getService(intent, IDeviceSettingsConfigProviderService.Stub::asInterface) .flatMapConcat { when (it) { is ServiceConnectionStatus.Connected -> flowOf( it.service.getDeviceSettingsConfig( deviceInfo { setBluetoothAddress(cachedDevice.address) } deviceInfo { setBluetoothAddress(cachedDevice.address) } ) ) ) ServiceConnectionStatus.Connecting -> flowOf() ServiceConnectionStatus.Failed -> flowOf(null) } } } .first() .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. */ /** Gets all device settings for the device. */ fun getDeviceSettingList(): Flow<List<DeviceSetting>> = 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. */ /** Gets the device settings with the ID for the device. */ fun getDeviceSetting(@DeviceSettingId deviceSettingId: Int): Flow<DeviceSetting?> = fun getDeviceSetting(@DeviceSettingId deviceSettingId: Int): Flow<DeviceSetting?> = getSettingIdToItemMapping().map { it[deviceSettingId] } settingIdToItemMapping.map { it[deviceSettingId] } /** Updates the device setting state for the device. */ /** Updates the device setting state for the device. */ suspend fun updateDeviceSettings( suspend fun updateDeviceSettings( @DeviceSettingId deviceSettingId: Int, @DeviceSettingId deviceSettingId: Int, deviceSettingPreferenceState: DeviceSettingPreferenceState, deviceSettingPreferenceState: DeviceSettingPreferenceState, ) { ) { getDeviceSettingsConfig()?.let { config -> if (!isServiceEnabled.await()) { Log.w(TAG, "Service is disabled") return } readConfig()?.let { config -> (config.mainContentItems + config.moreSettingsItems) (config.mainContentItems + config.moreSettingsItems) .find { it.settingId == deviceSettingId } .find { it.settingId == deviceSettingId } ?.let { ?.let { getSettingsProviderServices() getSettingsProviderServices() ?.get(EndPoint(it.packageName, it.className, it.intentAction)) ?.get(EndPoint(it.packageName, it.className, it.intentAction)) ?.filterNotNull() ?.filterIsInstance< ServiceConnectionStatus.Connected<IDeviceSettingsProviderService> >() ?.first() ?.first() } } ?.service ?.updateDeviceSettings( ?.updateDeviceSettings( deviceInfo { setBluetoothAddress(cachedDevice.address) }, deviceInfo { setBluetoothAddress(cachedDevice.address) }, DeviceSettingState.Builder() DeviceSettingState.Builder() .setSettingId(deviceSettingId) .setSettingId(deviceSettingId) .setPreferenceState(deviceSettingPreferenceState) .setPreferenceState(deviceSettingPreferenceState) .build() .build(), ) ) } } } } private suspend fun readConfig(): DeviceSettingsConfig? = config.await() private suspend fun getSettingsProviderServices(): private suspend fun getSettingsProviderServices(): Map<EndPoint, StateFlow<IDeviceSettingsProviderService?>>? = Map<EndPoint, StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>>? = getDeviceSettingsConfig() readConfig() ?.let { config -> ?.let { config -> (config.mainContentItems + config.moreSettingsItems).map { (config.mainContentItems + config.moreSettingsItems).map { EndPoint( EndPoint( packageName = it.packageName, packageName = it.packageName, className = it.className, className = it.className, intentAction = it.intentAction intentAction = it.intentAction, ) ) } } } } Loading @@ -150,43 +220,22 @@ class DeviceSettingServiceConnection( { it }, { it }, { endpoint -> { endpoint -> services.computeIfAbsent(endpoint) { services.computeIfAbsent(endpoint) { getService(endpoint.toIntent()) getService( .map { service -> endpoint.toIntent(), IDeviceSettingsProviderService.Stub.asInterface(service) IDeviceSettingsProviderService.Stub::asInterface, } ) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null) .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( private fun getDeviceSettingsFromService( cachedDevice: CachedBluetoothDevice, cachedDevice: CachedBluetoothDevice, service: IDeviceSettingsProviderService service: IDeviceSettingsProviderService, ): Flow<List<DeviceSetting>> { ): Flow<List<DeviceSetting>> { return callbackFlow { return callbackFlow { val listener = val listener = Loading @@ -202,51 +251,28 @@ class DeviceSettingServiceConnection( .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) .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 { return callbackFlow { val serviceConnection = val serviceConnection = object : ServiceConnection { object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { override fun onServiceConnected(name: ComponentName, service: IBinder) { launch { send(service) } launch { send(ServiceConnectionStatus.Connected(transform(service))) } } } override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) { launch { send(null) } launch { send(ServiceConnectionStatus.Connecting) } } } } } if (!context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)) { if (!context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)) { launch { send(null) } launch { send(ServiceConnectionStatus.Failed) } } } awaitClose { context.unbindService(serviceConnection) } 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? = private suspend fun tryGetEndpointFromMetadata(cachedDevice: CachedBluetoothDevice): EndPoint? = withContext(backgroundCoroutineContext) { withContext(backgroundCoroutineContext) { val packageName = val packageName = Loading @@ -257,29 +283,31 @@ class DeviceSettingServiceConnection( val className = val className = BluetoothUtils.getFastPairCustomizedField( BluetoothUtils.getFastPairCustomizedField( cachedDevice.device, cachedDevice.device, CONFIG_SERVICE_CLASS_NAME CONFIG_SERVICE_CLASS_NAME, ) ?: return@withContext null ) ?: return@withContext null val intentAction = val intentAction = BluetoothUtils.getFastPairCustomizedField( BluetoothUtils.getFastPairCustomizedField( cachedDevice.device, cachedDevice.device, CONFIG_SERVICE_INTENT_ACTION CONFIG_SERVICE_INTENT_ACTION, ) ?: return@withContext null ) ?: return@withContext null EndPoint(packageName, className, intentAction) 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 { private inline fun deviceInfo(block: DeviceInfo.Builder.() -> Unit): DeviceInfo { return DeviceInfo.Builder().apply { block() }.build() return DeviceInfo.Builder().apply { block() }.build() } } companion object { companion object { const val TAG = "DeviceSettingSrvConn" const val METADATA_FAST_PAIR_CUSTOMIZED_FIELDS: Int = 25 const val METADATA_FAST_PAIR_CUSTOMIZED_FIELDS: Int = 25 const val CONFIG_SERVICE_PACKAGE_NAME = "DEVICE_SETTINGS_CONFIG_PACKAGE_NAME" 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_CLASS_NAME = "DEVICE_SETTINGS_CONFIG_CLASS" const val CONFIG_SERVICE_INTENT_ACTION = "DEVICE_SETTINGS_CONFIG_ACTION" const val CONFIG_SERVICE_INTENT_ACTION = "DEVICE_SETTINGS_CONFIG_ACTION" val services = ConcurrentHashMap<EndPoint, StateFlow<IDeviceSettingsProviderService?>>() val services = ConcurrentHashMap< EndPoint, StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>, >() } } } }