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

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

Merge "Fallback to Slice if settings provider service is not enabled" into main

parents c53e36a6 5783c511
Loading
Loading
Loading
Loading
+19 −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;

parcelable DeviceSettingsProviderServiceStatus;
 No newline at end of file
+60 −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

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)
                }
            }
    }
}
+6 −4
Original line number Diff line number Diff line
@@ -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
+31 −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.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>
}
+122 −94
Original line number Diff line number Diff line
@@ -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(
    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>>,
            >()
    }
}
Loading