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

Commit bfe52e84 authored by Luna Zhang's avatar Luna Zhang
Browse files

Refactor BluetoothDetailsContentManager's bind method to handle all the

UI bindings

Bug: b/378513956
Test: BluetoothDetailsContentViewModelTest, BluetoothDetailsContentManagerTest
Flag: NONE refactor only
Change-Id: I0f9a297162853c2d9a673a6ecf7d877a92f969ef
parent 346557c1
Loading
Loading
Loading
Loading
+125 −15
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.bluetooth.qsdialog

import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.AccessibilityDelegate
@@ -39,20 +41,33 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.internal.R as InternalR
import com.android.internal.logging.UiEventLogger
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
import com.android.systemui.Prefs
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.bluetooth.ui.viewModel.BluetoothDetailsContentViewModel
import com.android.systemui.bluetooth.ui.viewModel.BluetoothTileDialogCallback
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.annotations.DeprecatedSysuiVisibleForTesting
import com.android.systemui.util.time.SystemClock
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

data class DeviceItemClick(val deviceItem: DeviceItem, val clickedView: View, val target: Target) {
@@ -68,14 +83,15 @@ class BluetoothDetailsContentManager
constructor(
    @Assisted private val initialUiProperties: BluetoothDetailsContentViewModel.UiProperties,
    @Assisted private val cachedContentHeight: Int,
    @Assisted private val bluetoothTileDialogCallback: BluetoothTileDialogCallback,
    @Assisted private val isInDialog: Boolean,
    @Assisted private val doneButtonCallback: () -> Unit,
    @Main private val mainDispatcher: CoroutineDispatcher,
    private val systemClock: SystemClock,
    private val uiEventLogger: UiEventLogger,
    private val logger: BluetoothTileDialogLogger,
) {
    private val dialogTransitionAnimator: DialogTransitionAnimator,
    private val activityStarter: ActivityStarter,
) : BluetoothTileDialogCallback {

    private val mutableBluetoothStateToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null)
    @DeprecatedSysuiVisibleForTesting
@@ -107,6 +123,8 @@ constructor(

    private var lastItemRow: Int = -1

    private lateinit var coroutineScope: CoroutineScope

    // UI Components
    private lateinit var contentView: View
    private lateinit var doneButton: Button
@@ -128,14 +146,20 @@ constructor(
        fun create(
            initialUiProperties: BluetoothDetailsContentViewModel.UiProperties,
            cachedContentHeight: Int,
            dialogCallback: BluetoothTileDialogCallback,
            isInDialog: Boolean,
            doneButtonCallback: () -> Unit,
        ): BluetoothDetailsContentManager
    }

    fun bind(contentView: View) {
    fun bind(
        contentView: View,
        dialog: SystemUIDialog?,
        coroutineScope: CoroutineScope,
        detailsUIState: BluetoothDetailsContentViewModel.DetailsUIState,
    ) {

        this.contentView = contentView
        this.coroutineScope = coroutineScope

        doneButton = contentView.requireViewById(R.id.done_button)
        bluetoothToggle = contentView.requireViewById(R.id.bluetooth_toggle)
@@ -159,12 +183,10 @@ constructor(
        setupDoneButton()

        subtitleTextView.text = contentView.context.getString(initialUiProperties.subTitleResId)
        seeAllButton.setOnClickListener { bluetoothTileDialogCallback.onSeeAllClicked(it) }
        pairNewDeviceButton.setOnClickListener {
            bluetoothTileDialogCallback.onPairNewDeviceClicked(it)
        }
        seeAllButton.setOnClickListener { onSeeAllClicked(it) }
        pairNewDeviceButton.setOnClickListener { onPairNewDeviceClicked(it) }
        audioSharingButton.apply {
            setOnClickListener { bluetoothTileDialogCallback.onAudioSharingButtonClicked(it) }
            setOnClickListener { onAudioSharingButtonClicked(it) }
            accessibilityDelegate =
                object : AccessibilityDelegate() {
                    override fun onInitializeAccessibilityNodeInfo(
@@ -189,6 +211,47 @@ constructor(
                resources.getDimensionPixelSize(initialUiProperties.scrollViewMinHeightResId)
            layoutParams.height = maxOf(cachedContentHeight, minimumHeight)
        }
        updateDetailsUI(dialog, detailsUIState)
    }

    private fun updateDetailsUI(
        dialog: SystemUIDialog?,
        detailsUIState: BluetoothDetailsContentViewModel.DetailsUIState,
    ) {
        coroutineScope.launch {
            var updateDialogUiJob: Job? = null

            detailsUIState.deviceItem
                .filterNotNull()
                .onEach {
                    updateDialogUiJob?.cancel()
                    updateDialogUiJob = launch {
                        onDeviceItemUpdated(it.deviceItem, it.showSeeAll, it.showPairNewDevice)
                    }
                }
                .launchIn(this)

            detailsUIState.shouldAnimateProgressBar
                .filterNotNull()
                .onEach { animateProgressBar(it) }
                .launchIn(this)

            detailsUIState.audioSharingButton
                .filterNotNull()
                .onEach { onAudioSharingButtonUpdated(it.visibility, it.label, it.isActive) }
                .launchIn(this)

            detailsUIState.bluetoothState
                .filterNotNull()
                .onEach { onBluetoothStateUpdated(it.isEnabled, it.uiProperties) }
                .launchIn(this)

            detailsUIState.bluetoothAutoOn
                .filterNotNull()
                .onEach { onBluetoothAutoOnUpdated(it.isEnabled, it.infoResId) }
                .launchIn(this)
            produce<Unit> { awaitClose { dialog?.cancel() } }
        }
    }

    fun start() {
@@ -199,7 +262,31 @@ constructor(
        mutableContentHeight.value = scrollViewContent.measuredHeight
    }

    internal suspend fun animateProgressBar(animate: Boolean) {
    override fun onSeeAllClicked(view: View) {
        uiEventLogger.log(BluetoothTileDialogUiEvent.SEE_ALL_CLICKED)
        startSettingsActivity(Intent(ACTION_PREVIOUSLY_CONNECTED_DEVICE), view)
    }

    override fun onPairNewDeviceClicked(view: View) {
        uiEventLogger.log(BluetoothTileDialogUiEvent.PAIR_NEW_DEVICE_CLICKED)
        startSettingsActivity(Intent(ACTION_PAIR_NEW_DEVICE), view)
    }

    override fun onAudioSharingButtonClicked(view: View) {
        uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_AUDIO_SHARING_BUTTON_CLICKED)
        val intent =
            Intent(ACTION_AUDIO_SHARING).apply {
                putExtra(
                    EXTRA_SHOW_FRAGMENT_ARGUMENTS,
                    Bundle().apply {
                        putBoolean(LocalBluetoothLeBroadcast.EXTRA_START_LE_AUDIO_SHARING, true)
                    },
                )
            }
        startSettingsActivity(intent, view)
    }

    suspend fun animateProgressBar(animate: Boolean) {
        withContext(mainDispatcher) {
            if (animate) {
                showProgressBar()
@@ -210,7 +297,7 @@ constructor(
        }
    }

    internal suspend fun onDeviceItemUpdated(
    suspend fun onDeviceItemUpdated(
        deviceItem: List<DeviceItem>,
        showSeeAll: Boolean,
        showPairNewDevice: Boolean,
@@ -236,7 +323,7 @@ constructor(
        }
    }

    internal fun onBluetoothStateUpdated(
    fun onBluetoothStateUpdated(
        isEnabled: Boolean,
        uiProperties: BluetoothDetailsContentViewModel.UiProperties,
    ) {
@@ -249,12 +336,12 @@ constructor(
        autoOnToggleLayout.visibility = uiProperties.autoOnToggleVisibility
    }

    internal fun onBluetoothAutoOnUpdated(isEnabled: Boolean, @StringRes infoResId: Int) {
    fun onBluetoothAutoOnUpdated(isEnabled: Boolean, @StringRes infoResId: Int) {
        autoOnToggle.isChecked = isEnabled
        autoOnToggleInfoTextView.text = contentView.context.getString(infoResId)
    }

    internal fun onAudioSharingButtonUpdated(visibility: Int, label: String?, isActive: Boolean) {
    fun onAudioSharingButtonUpdated(visibility: Int, label: String?, isActive: Boolean) {
        audioSharingButton.apply {
            this.visibility = visibility
            label?.let { text = it }
@@ -262,6 +349,19 @@ constructor(
        }
    }

    fun startSettingsActivity(intent: Intent, view: View) {
        if (coroutineScope.isActive) {
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
            val controller = dialogTransitionAnimator.createActivityTransitionController(view)
            // The controller will be null when the screen is locked and going to show the
            // primary bouncer. In this case we dismiss the dialog manually.
            if (controller == null) {
                coroutineScope.cancel()
            }
            activityStarter.postStartActivityDismissingKeyguard(intent, 0, controller)
        }
    }

    private fun setupToggle() {
        bluetoothToggle.setOnCheckedChangeListener { view, isChecked ->
            mutableBluetoothStateToggle.value = isChecked
@@ -447,6 +547,8 @@ constructor(
    }

    internal companion object {
        private const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"
        const val CONTENT_HEIGHT_PREF_KEY = Prefs.Key.BLUETOOTH_TILE_DIALOG_CONTENT_HEIGHT
        const val MIN_HEIGHT_CHANGE_INTERVAL_MS = 800L
        const val ACTION_BLUETOOTH_DEVICE_DETAILS =
            "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS"
@@ -463,3 +565,11 @@ constructor(
        }
    }
}

interface BluetoothTileDialogCallback {
    fun onSeeAllClicked(view: View)

    fun onPairNewDeviceClicked(view: View)

    fun onAudioSharingButtonClicked(view: View)
}
+0 −5
Original line number Diff line number Diff line
@@ -20,7 +20,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import com.android.internal.logging.UiEventLogger
import com.android.systemui.bluetooth.ui.viewModel.BluetoothDetailsContentViewModel
import com.android.systemui.bluetooth.ui.viewModel.BluetoothTileDialogCallback
import com.android.systemui.qs.flags.QsDetailedView
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor
@@ -36,7 +35,6 @@ class BluetoothTileDialogDelegate
constructor(
    @Assisted private val initialUiProperties: BluetoothDetailsContentViewModel.UiProperties,
    @Assisted private val cachedContentHeight: Int,
    @Assisted private val bluetoothTileDialogCallback: BluetoothTileDialogCallback,
    @Assisted private val dismissListener: Runnable,
    private val uiEventLogger: UiEventLogger,
    private val systemuiDialogFactory: SystemUIDialog.Factory,
@@ -52,7 +50,6 @@ constructor(
        fun create(
            initialUiProperties: BluetoothDetailsContentViewModel.UiProperties,
            cachedContentHeight: Int,
            dialogCallback: BluetoothTileDialogCallback,
            dismissListener: Runnable,
        ): BluetoothTileDialogDelegate
    }
@@ -81,13 +78,11 @@ constructor(
            bluetoothDetailsContentManagerFactory.create(
                initialUiProperties,
                cachedContentHeight,
                bluetoothTileDialogCallback,
                /* isInDialog= */ true,
                /* doneButtonCallback= */ fun() {
                    dialog.dismiss()
                },
            )
        contentManager.bind(dialog.requireViewById(R.id.root))
    }

    override fun onStart(dialog: SystemUIDialog) {
+83 −106

File changed.

Preview size limit exceeded, changes collapsed.

+274 −157

File changed.

Preview size limit exceeded, changes collapsed.

+12 −65
Original line number Diff line number Diff line
@@ -24,8 +24,6 @@ import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.flags.Flags
import com.android.systemui.Flags.FLAG_QS_TILE_DETAILED_VIEW
@@ -36,15 +34,12 @@ import com.android.systemui.bluetooth.ui.viewModel.BluetoothDetailsContentViewMo
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.testKosmos
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.kotlin.getMutableStateFlow
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.volume.domain.interactor.audioModeInteractor
import com.google.common.truth.Truth.assertThat
@@ -59,13 +54,15 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.eq
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -86,16 +83,8 @@ class BluetoothDetailsContentViewModelTest : SysuiTestCase() {

    @Mock private lateinit var deviceItemActionInteractor: DeviceItemActionInteractor

    @Mock private lateinit var activityStarter: ActivityStarter

    @Mock private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator

    @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice

    @Mock private lateinit var deviceItem: DeviceItem

    @Mock private lateinit var uiEventLogger: UiEventLogger

    @Mock private lateinit var bluetoothAdapter: BluetoothAdapter

    @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
@@ -103,8 +92,7 @@ class BluetoothDetailsContentViewModelTest : SysuiTestCase() {
    @Mock private lateinit var bluetoothTileDialogLogger: BluetoothTileDialogLogger

    @Mock
    private lateinit var mBluetoothTileDialogDelegateDelegateFactory:
        BluetoothTileDialogDelegate.Factory
    private lateinit var mBluetoothTileDialogDelegateFactory: BluetoothTileDialogDelegate.Factory

    @Mock private lateinit var bluetoothTileDialogDelegate: BluetoothTileDialogDelegate

@@ -154,14 +142,12 @@ class BluetoothDetailsContentViewModelTest : SysuiTestCase() {
                kosmos.audioSharingButtonViewModelFactory,
                bluetoothDeviceMetadataInteractor,
                mDialogTransitionAnimator,
                activityStarter,
                uiEventLogger,
                bluetoothTileDialogLogger,
                testScope.backgroundScope,
                dispatcher,
                dispatcher,
                sharedPreferences,
                mBluetoothTileDialogDelegateDelegateFactory,
                mBluetoothTileDialogDelegateFactory,
                bluetoothDetailsContentManagerFactory,
            )
        whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(MutableSharedFlow())
@@ -169,22 +155,15 @@ class BluetoothDetailsContentViewModelTest : SysuiTestCase() {
            .thenReturn(MutableStateFlow(Unit).asStateFlow())
        whenever(deviceItemInteractor.showSeeAllUpdate).thenReturn(getMutableStateFlow(false))
        whenever(bluetoothDeviceMetadataInteractor.metadataUpdate).thenReturn(MutableSharedFlow())
        whenever(mBluetoothTileDialogDelegateDelegateFactory.create(any(), anyInt(), any(), any()))
        whenever(mBluetoothTileDialogDelegateFactory.create(any(), anyInt(), any()))
            .thenReturn(bluetoothTileDialogDelegate)
        whenever(bluetoothTileDialogDelegate.createDialog()).thenReturn(sysuiDialog)
        whenever(bluetoothTileDialogDelegate.contentManager)
            .thenReturn(bluetoothDetailsContentManager)
        whenever(
                bluetoothDetailsContentManagerFactory.create(
                    any(),
                    anyInt(),
                    any(),
                    anyBoolean(),
                    any(),
                )
            )
        whenever(bluetoothDetailsContentManagerFactory.create(any(), anyInt(), anyBoolean(), any()))
            .thenReturn(bluetoothDetailsContentManager)
        whenever(sysuiDialog.context).thenReturn(mContext)
        whenever<Any?>(sysuiDialog.requireViewById(anyInt())).thenReturn(mockView)
        whenever(bluetoothDetailsContentManager.bluetoothStateToggle)
            .thenReturn(getMutableStateFlow(false))
        whenever(bluetoothDetailsContentManager.deviceItemClick)
@@ -224,7 +203,7 @@ class BluetoothDetailsContentViewModelTest : SysuiTestCase() {
            bluetoothDetailsContentViewModel.bindDetailsView(mockView)
            runCurrent()

            verify(bluetoothDetailsContentManager).bind(mockView)
            verify(bluetoothDetailsContentManager).bind(eq(mockView), eq(null), any(), any())
            verify(bluetoothDetailsContentManager).start()
        }
    }
@@ -250,7 +229,7 @@ class BluetoothDetailsContentViewModelTest : SysuiTestCase() {
                bluetoothDetailsContentViewModel.bindDetailsView(mockView)
                runCurrent()

                verify(bluetoothDetailsContentManager).bind(mockView)
                verify(bluetoothDetailsContentManager).bind(eq(mockView), eq(null), any(), any())
                verify(bluetoothDetailsContentManager).start()
            }
        }
@@ -278,38 +257,6 @@ class BluetoothDetailsContentViewModelTest : SysuiTestCase() {
        }
    }

    @Test
    fun testStartSettingsActivity_activityLaunched_dialogDismissed() {
        testScope.runTest {
            whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
            bluetoothDetailsContentViewModel.showDialog(null)
            runCurrent()

            val clickedView = View(context)
            bluetoothDetailsContentViewModel.onPairNewDeviceClicked(clickedView)

            verify(uiEventLogger).log(BluetoothTileDialogUiEvent.PAIR_NEW_DEVICE_CLICKED)
            verify(activityStarter).postStartActivityDismissingKeyguard(any(), anyInt(), nullable())
        }
    }

    @Test
    @EnableSceneContainer
    @EnableFlags(FLAG_QS_TILE_DETAILED_VIEW)
    fun testStartSettingsActivity_activityLaunched_detailsViewDismissed() {
        testScope.runTest {
            whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
            bluetoothDetailsContentViewModel.bindDetailsView(mockView)
            runCurrent()

            val clickedView = View(context)
            bluetoothDetailsContentViewModel.onPairNewDeviceClicked(clickedView)

            verify(uiEventLogger).log(BluetoothTileDialogUiEvent.PAIR_NEW_DEVICE_CLICKED)
            verify(activityStarter).postStartActivityDismissingKeyguard(any(), anyInt(), nullable())
        }
    }

    @Test
    fun testBuildUiProperties_bluetoothOn_shouldHideAutoOn() {
        testScope.runTest {
Loading