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

Commit e385589f authored by chelseahao's avatar chelseahao Committed by Chelsea Hao
Browse files

Read and write auto on toggle value.

Test: atest -c com.android.systemui.qs.tiles.dialog.bluetooth
Bug: b/316822488 b/316985153
Flag: ACONFIG com.android.settingslib.flags.bluetooth_qs_tile_dialog_auto_on_toggle DISABLED
Change-Id: I2e87eb596e1433a70dd6ac46ee98bd63ed4881f8
parent cf3544bb
Loading
Loading
Loading
Loading
+61 −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.systemui.qs.tiles.dialog.bluetooth

import android.util.Log
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map

/** Interactor class responsible for interacting with the Bluetooth Auto-On feature. */
@SysUISingleton
class BluetoothAutoOnInteractor
@Inject
constructor(
    private val bluetoothAutoOnRepository: BluetoothAutoOnRepository,
) {

    val isEnabled = bluetoothAutoOnRepository.getValue.map { it == ENABLED }.distinctUntilChanged()

    /**
     * Checks if the auto on value is present in the repository.
     *
     * @return `true` if a value is present (i.e, the feature is enabled by the Bluetooth server).
     */
    suspend fun isValuePresent(): Boolean = bluetoothAutoOnRepository.isValuePresent()

    /**
     * Sets enabled or disabled based on the provided value.
     *
     * @param value `true` to enable the feature, `false` to disable it.
     */
    suspend fun setEnabled(value: Boolean) {
        if (!isValuePresent()) {
            Log.e(TAG, "Trying to set toggle value while feature not available.")
        } else {
            val newValue = if (value) ENABLED else DISABLED
            bluetoothAutoOnRepository.setValue(newValue)
        }
    }

    companion object {
        private const val TAG = "BluetoothAutoOnInteractor"
        const val DISABLED = 0
        const val ENABLED = 1
    }
}
+101 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.systemui.qs.tiles.dialog.bluetooth

import android.os.UserHandle
import android.util.Log
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.withContext

/** Repository class responsible for managing the Bluetooth Auto-On feature settings. */
// TODO(b/316822488): Handle multi-user
@SysUISingleton
class BluetoothAutoOnRepository
@Inject
constructor(
    private val secureSettings: SecureSettings,
    private val userRepository: UserRepository,
    @Application private val coroutineScope: CoroutineScope,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
) {
    // Flow representing the auto on setting value
    internal val getValue: Flow<Int> =
        secureSettings
            .observerFlow(UserHandle.USER_SYSTEM, SETTING_NAME)
            .onStart { emit(Unit) }
            .map {
                if (userRepository.getSelectedUserInfo().id != UserHandle.USER_SYSTEM) {
                    Log.i(TAG, "Current user is not USER_SYSTEM. Multi-user is not supported")
                    return@map UNSET
                }
                secureSettings.getIntForUser(SETTING_NAME, UNSET, UserHandle.USER_SYSTEM)
            }
            .distinctUntilChanged()
            .flowOn(backgroundDispatcher)
            .shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0))

    /**
     * Checks if the auto on setting value is ever set for the current user.
     *
     * @return `true` if the setting value is not UNSET, `false` otherwise.
     */
    suspend fun isValuePresent(): Boolean =
        withContext(backgroundDispatcher) {
            if (userRepository.getSelectedUserInfo().id != UserHandle.USER_SYSTEM) {
                Log.i(TAG, "Current user is not USER_SYSTEM. Multi-user is not supported")
                false
            } else {
                secureSettings.getIntForUser(SETTING_NAME, UNSET, UserHandle.USER_SYSTEM) != UNSET
            }
        }

    /**
     * Sets the Bluetooth Auto-On setting value for the current user.
     *
     * @param value The new setting value to be applied.
     */
    suspend fun setValue(value: Int) {
        withContext(backgroundDispatcher) {
            if (userRepository.getSelectedUserInfo().id != UserHandle.USER_SYSTEM) {
                Log.i(TAG, "Current user is not USER_SYSTEM. Multi-user is not supported")
            } else {
                secureSettings.putIntForUser(SETTING_NAME, value, UserHandle.USER_SYSTEM)
            }
        }
    }

    companion object {
        private const val TAG = "BluetoothAutoOnRepository"
        const val SETTING_NAME = "bluetooth_automatic_turn_on"
        const val UNSET = -1
    }
}
+24 −0
Original line number Diff line number Diff line
@@ -71,6 +71,10 @@ constructor(
    internal val bluetoothStateToggle
        get() = mutableBluetoothStateToggle.asStateFlow()

    private val mutableBluetoothAutoOnToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null)
    internal val bluetoothAutoOnToggle
        get() = mutableBluetoothAutoOnToggle.asStateFlow()

    private val mutableDeviceItemClick: MutableSharedFlow<DeviceItem> =
        MutableSharedFlow(extraBufferCapacity = 1)
    internal val deviceItemClick
@@ -89,6 +93,7 @@ constructor(

    private lateinit var toggleView: Switch
    private lateinit var subtitleTextView: TextView
    private lateinit var autoOnToggle: Switch
    private lateinit var autoOnToggleView: View
    private lateinit var doneButton: View
    private lateinit var seeAllButton: View
@@ -109,6 +114,7 @@ constructor(

        toggleView = requireViewById(R.id.bluetooth_toggle)
        subtitleTextView = requireViewById(R.id.bluetooth_tile_dialog_subtitle) as TextView
        autoOnToggle = requireViewById(R.id.bluetooth_auto_on_toggle)
        autoOnToggleView = requireViewById(R.id.bluetooth_auto_on_toggle_layout)
        doneButton = requireViewById(R.id.done_button)
        seeAllButton = requireViewById(R.id.see_all_button)
@@ -195,6 +201,16 @@ constructor(
        autoOnToggleView.visibility = uiProperties.autoOnToggleVisibility
    }

    internal fun onBluetoothAutoOnUpdated(isEnabled: Boolean) {
        if (::autoOnToggle.isInitialized) {
            autoOnToggle.apply {
                isChecked = isEnabled
                setEnabled(true)
                alpha = ENABLED_ALPHA
            }
        }
    }

    private fun setupToggle() {
        toggleView.isChecked = bluetoothToggleInitialValue
        toggleView.setOnCheckedChangeListener { view, isChecked ->
@@ -208,6 +224,14 @@ constructor(
        }

        autoOnToggleView.visibility = initialUiProperties.autoOnToggleVisibility
        autoOnToggle.setOnCheckedChangeListener { view, isChecked ->
            mutableBluetoothAutoOnToggle.value = isChecked
            view.apply {
                isEnabled = false
                alpha = DISABLED_ALPHA
            }
            uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_AUTO_ON_TOGGLE_CLICKED)
        }
    }

    private fun setupRecyclerView() {
+2 −1
Original line number Diff line number Diff line
@@ -31,7 +31,8 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent
    @UiEvent(doc = "Saved clicked to connect") SAVED_DEVICE_CONNECT(1500),
    @UiEvent(doc = "Active device clicked to disconnect") ACTIVE_DEVICE_DISCONNECT(1507),
    @UiEvent(doc = "Connected other device clicked to disconnect")
    CONNECTED_OTHER_DEVICE_DISCONNECT(1508);
    CONNECTED_OTHER_DEVICE_DISCONNECT(1508),
    @UiEvent(doc = "The auto on toggle is clicked") BLUETOOTH_AUTO_ON_TOGGLE_CLICKED(1617);

    override fun getId() = metricId
}
+35 −5
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.annotation.DimenRes
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.logging.UiEventLogger
import com.android.settingslib.flags.Flags.bluetoothQsTileDialogAutoOnToggle
@@ -63,6 +64,7 @@ internal class BluetoothTileDialogViewModel
constructor(
    private val deviceItemInteractor: DeviceItemInteractor,
    private val bluetoothStateInteractor: BluetoothStateInteractor,
    private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor,
    private val dialogTransitionAnimator: DialogTransitionAnimator,
    private val activityStarter: ActivityStarter,
    private val systemClock: SystemClock,
@@ -148,7 +150,10 @@ constructor(
                bluetoothStateInteractor.bluetoothStateUpdate
                    .filterNotNull()
                    .onEach {
                        dialog.onBluetoothStateUpdated(it, UiProperties.build(it))
                        dialog.onBluetoothStateUpdated(
                            it,
                            UiProperties.build(it, isAutoOnToggleFeatureAvailable())
                        )
                        updateDeviceItemJob?.cancel()
                        updateDeviceItemJob = launch {
                            deviceItemInteractor.updateDeviceItems(
@@ -182,6 +187,21 @@ constructor(
                    }
                    .launchIn(this)

                if (isAutoOnToggleFeatureAvailable()) {
                    // bluetoothAutoOnUpdate is emitted when bluetooth auto on on/off state is
                    // changed.
                    bluetoothAutoOnInteractor.isEnabled
                        .onEach { dialog.onBluetoothAutoOnUpdated(it) }
                        .launchIn(this)

                    // bluetoothAutoOnToggle is emitted when user toggles the bluetooth auto on
                    // switch, send the new value to the bluetoothAutoOnInteractor.
                    dialog.bluetoothAutoOnToggle
                        .filterNotNull()
                        .onEach { bluetoothAutoOnInteractor.setEnabled(it) }
                        .launchIn(this)
                }

                produce<Unit> { awaitClose { dialog.cancel() } }
            }
    }
@@ -197,7 +217,10 @@ constructor(

        return BluetoothTileDialog(
                bluetoothStateInteractor.isBluetoothEnabled,
                UiProperties.build(bluetoothStateInteractor.isBluetoothEnabled),
                UiProperties.build(
                    bluetoothStateInteractor.isBluetoothEnabled,
                    isAutoOnToggleFeatureAvailable()
                ),
                cachedContentHeight,
                this@BluetoothTileDialogViewModel,
                mainDispatcher,
@@ -249,6 +272,10 @@ constructor(
        }
    }

    @VisibleForTesting
    internal suspend fun isAutoOnToggleFeatureAvailable() =
        bluetoothQsTileDialogAutoOnToggle() && bluetoothAutoOnInteractor.isValuePresent()

    companion object {
        private const val INTERACTION_JANK_TAG = "bluetooth_tile_dialog"
        private const val CONTENT_HEIGHT_PREF_KEY = Prefs.Key.BLUETOOTH_TILE_DIALOG_CONTENT_HEIGHT
@@ -263,14 +290,17 @@ constructor(
        @DimenRes val scrollViewMinHeightResId: Int,
    ) {
        companion object {
            internal fun build(isBluetoothEnabled: Boolean) =
            internal fun build(
                isBluetoothEnabled: Boolean,
                isAutoOnToggleFeatureAvailable: Boolean
            ) =
                UiProperties(
                    subTitleResId = getSubtitleResId(isBluetoothEnabled),
                    autoOnToggleVisibility =
                        if (bluetoothQsTileDialogAutoOnToggle() && !isBluetoothEnabled) VISIBLE
                        if (isAutoOnToggleFeatureAvailable && !isBluetoothEnabled) VISIBLE
                        else GONE,
                    scrollViewMinHeightResId =
                        if (bluetoothQsTileDialogAutoOnToggle())
                        if (isAutoOnToggleFeatureAvailable)
                            R.dimen.bluetooth_dialog_scroll_view_min_height_with_auto_on
                        else R.dimen.bluetooth_dialog_scroll_view_min_height
                )
Loading