[SB Refactor] Connect the old pipeline and new pipeline visibility

calculations together for wifi.

The _new_ pipeline has the source of truth for whether the wifi icon
*should* be visible. But, the _old_ view system has the source of truth
for whether the wifi icon *can* be visible (i.e. if there's enough
space for it).

This CL hooks up those two systems together by having
`WifiViewBinder#bind` return a `Binding` interface implementation,
similar to `KeyguardButtomAreaViewBinder#bind`. This allows them to
communicate with each other and correctly change the view's visibility.

Bug: 238425913
Test: manual: Go into airplane mode and verify that the wifi icon
disappears completely and there's no empty gap in the status bar.
Test: manual: Go into a state where there's not enough room for the wifi
icon and verify that just a tinted dot shows up. See demo video in
b/238425913#comment30.
Test: statusbar.pipeline tests

Change-Id: I9d421a4afb503c9327578f777561a4d38bf2fd48
This commit is contained in:
Caitlin Shkuratov
2022-09-23 14:20:41 +00:00
parent 1cb44532d7
commit a0fbcf44f1
10 changed files with 287 additions and 31 deletions

View File

@@ -131,6 +131,9 @@
<!-- For StatusIconContainer to tag its icon views -->
<item type="id" name="status_bar_view_state_tag" />
<!-- Status bar -->
<item type="id" name="status_bar_dot" />
<!-- Default display cutout on the physical top of screen -->
<item type="id" name="display_cutout" />
<item type="id" name="display_cutout_left" />

View File

@@ -38,7 +38,15 @@ public interface StatusIconDisplayable extends DarkReceiver {
@StatusBarIconView.VisibleState
int getVisibleState();
/**
* Returns true if this icon should be visible if there's space, and false otherwise.
*
* Note that this doesn't necessarily mean it *will* be visible. It's possible that there are
* more icons than space, in which case this icon might just show a dot or might be completely
* hidden. {@link #getVisibleState} will return the icon's actual visible status.
*/
boolean isIconVisible();
default boolean isIconBlocked() {
return false;
}

View File

@@ -26,10 +26,15 @@ import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.R
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@@ -44,6 +49,19 @@ import kotlinx.coroutines.launch
@OptIn(InternalCoroutinesApi::class)
object WifiViewBinder {
/**
* Defines interface for an object that acts as the binding between the view and its view-model.
*
* Users of the [WifiViewBinder] class should use this to control the binder after it is bound.
*/
interface Binding {
/** Returns true if the wifi icon should be visible and false otherwise. */
fun getShouldIconBeVisible(): Boolean
/** Notifies that the visibility state has changed. */
fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int)
}
/**
* Binds the view to the appropriate view-model based on the given location. The view will
* continue to be updated following updates from the view-model.
@@ -53,8 +71,8 @@ object WifiViewBinder {
view: ViewGroup,
wifiViewModel: WifiViewModel,
location: StatusBarLocation,
) {
when (location) {
): Binding {
return when (location) {
StatusBarLocation.HOME -> bind(view, wifiViewModel.home)
StatusBarLocation.KEYGUARD -> bind(view, wifiViewModel.keyguard)
StatusBarLocation.QS -> bind(view, wifiViewModel.qs)
@@ -66,8 +84,10 @@ object WifiViewBinder {
private fun bind(
view: ViewGroup,
viewModel: LocationBasedWifiViewModel,
) {
): Binding {
val groupView = view.requireViewById<ViewGroup>(R.id.wifi_group)
val iconView = view.requireViewById<ImageView>(R.id.wifi_signal)
val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot)
val activityInView = view.requireViewById<ImageView>(R.id.wifi_in)
val activityOutView = view.requireViewById<ImageView>(R.id.wifi_out)
val activityContainerView = view.requireViewById<View>(R.id.inout_container)
@@ -75,14 +95,21 @@ object WifiViewBinder {
view.isVisible = true
iconView.isVisible = true
// TODO(b/238425913): We should log this visibility state.
@StatusBarIconView.VisibleState
val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN)
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.wifiIcon.distinctUntilChanged().collect { wifiIcon ->
// TODO(b/238425913): Right now, if !isVisible, there's just an empty space
// where the wifi icon would be. We need to pipe isVisible through to
// [ModernStatusBarWifiView.isIconVisible], which is what actually makes
// the view GONE.
visibilityState.collect { visibilityState ->
groupView.isVisible = visibilityState == STATE_ICON
dotView.isVisible = visibilityState == STATE_DOT
}
}
launch {
viewModel.wifiIcon.collect { wifiIcon ->
view.isVisible = wifiIcon != null
wifiIcon?.let { IconViewBinder.bind(wifiIcon, iconView) }
}
@@ -94,6 +121,7 @@ object WifiViewBinder {
iconView.imageTintList = tintList
activityInView.imageTintList = tintList
activityOutView.imageTintList = tintList
dotView.setDecorColor(tint)
}
}
@@ -116,5 +144,15 @@ object WifiViewBinder {
}
}
}
return object : Binding {
override fun getShouldIconBeVisible(): Boolean {
return viewModel.wifiIcon.value != null
}
override fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) {
visibilityState.value = state
}
}
}
}

View File

@@ -19,11 +19,13 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.view
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.Gravity
import android.view.LayoutInflater
import com.android.systemui.R
import com.android.systemui.statusbar.BaseStatusBarWifiView
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.pipeline.wifi.ui.binder.WifiViewBinder
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
@@ -38,6 +40,17 @@ class ModernStatusBarWifiView(
) : BaseStatusBarWifiView(context, attrs) {
private lateinit var slot: String
private lateinit var binding: WifiViewBinder.Binding
@StatusBarIconView.VisibleState
private var iconVisibleState: Int = STATE_HIDDEN
set(value) {
if (field == value) {
return
}
field = value
binding.onVisibilityStateChanged(value)
}
override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) {
// TODO(b/238425913)
@@ -54,23 +67,44 @@ class ModernStatusBarWifiView(
}
override fun setVisibleState(@StatusBarIconView.VisibleState state: Int, animate: Boolean) {
// TODO(b/238425913)
iconVisibleState = state
}
@StatusBarIconView.VisibleState
override fun getVisibleState(): Int {
// TODO(b/238425913)
return STATE_ICON
return iconVisibleState
}
override fun isIconVisible(): Boolean {
// TODO(b/238425913)
return true
return binding.getShouldIconBeVisible()
}
/** Set the slot name for this view. */
private fun setSlot(slotName: String) {
this.slot = slotName
private fun initView(
slotName: String,
wifiViewModel: WifiViewModel,
location: StatusBarLocation,
) {
slot = slotName
initDotView()
binding = WifiViewBinder.bind(this, wifiViewModel, location)
}
// Mostly duplicated from [com.android.systemui.statusbar.StatusBarWifiView].
private fun initDotView() {
// TODO(b/238425913): Could we just have this dot view be part of
// R.layout.new_status_bar_wifi_group with a dot drawable so we don't need to inflate it
// manually? Would that not work with animations?
val dotView = StatusBarIconView(mContext, slot, null).also {
it.id = R.id.status_bar_dot
// Hard-code this view to always be in the DOT state so that whenever it's visible it
// will show a dot
it.visibleState = STATE_DOT
}
val width = mContext.resources.getDimensionPixelSize(R.dimen.status_bar_icon_size)
val lp = LayoutParams(width, width)
lp.gravity = Gravity.CENTER_VERTICAL or Gravity.START
addView(dotView, lp)
}
companion object {
@@ -89,8 +123,7 @@ class ModernStatusBarWifiView(
LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null)
as ModernStatusBarWifiView
).also {
it.setSlot(slot)
WifiViewBinder.bind(it, wifiViewModel, location)
it.initView(slot, wifiViewModel, location)
}
}
}

View File

@@ -20,6 +20,7 @@ import android.graphics.Color
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/**
* A view model for the wifi icon shown on the "home" page (aka, when the device is unlocked and not
@@ -27,7 +28,7 @@ import kotlinx.coroutines.flow.Flow
*/
class HomeWifiViewModel(
statusBarPipelineFlags: StatusBarPipelineFlags,
wifiIcon: Flow<Icon?>,
wifiIcon: StateFlow<Icon?>,
isActivityInViewVisible: Flow<Boolean>,
isActivityOutViewVisible: Flow<Boolean>,
isActivityContainerVisible: Flow<Boolean>,

View File

@@ -20,11 +20,12 @@ import android.graphics.Color
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/** A view model for the wifi icon shown on keyguard (lockscreen). */
class KeyguardWifiViewModel(
statusBarPipelineFlags: StatusBarPipelineFlags,
wifiIcon: Flow<Icon?>,
wifiIcon: StateFlow<Icon?>,
isActivityInViewVisible: Flow<Boolean>,
isActivityOutViewVisible: Flow<Boolean>,
isActivityContainerVisible: Flow<Boolean>,

View File

@@ -20,6 +20,7 @@ import android.graphics.Color
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOf
/**
@@ -33,7 +34,7 @@ abstract class LocationBasedWifiViewModel(
debugTint: Int,
/** The wifi icon that should be displayed. Null if we shouldn't display any icon. */
val wifiIcon: Flow<Icon?>,
val wifiIcon: StateFlow<Icon?>,
/** True if the activity in view should be visible. */
val isActivityInViewVisible: Flow<Boolean>,

View File

@@ -20,11 +20,12 @@ import android.graphics.Color
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/** A view model for the wifi icon shown in quick settings (when the shade is pulled down). */
class QsWifiViewModel(
statusBarPipelineFlags: StatusBarPipelineFlags,
wifiIcon: Flow<Icon?>,
wifiIcon: StateFlow<Icon?>,
isActivityInViewVisible: Flow<Boolean>,
isActivityOutViewVisible: Flow<Boolean>,
isActivityContainerVisible: Flow<Boolean>,

View File

@@ -41,6 +41,7 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
@@ -120,7 +121,7 @@ constructor(
.stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null)
/** The wifi icon that should be displayed. Null if we shouldn't display any icon. */
private val wifiIcon: Flow<Icon?> =
private val wifiIcon: StateFlow<Icon?> =
combine(
interactor.isEnabled,
interactor.isForceHidden,

View File

@@ -16,39 +16,208 @@
package com.android.systemui.statusbar.pipeline.wifi.ui.view
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import android.testing.ViewUtils
import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.lifecycle.InstantTaskExecutorRule
import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.util.Assert
import com.android.systemui.util.mockito.mock
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(JUnit4::class)
@RunWithLooper
@RunWith(AndroidTestingRunner::class)
@RunWithLooper(setAsMainLooper = true)
class ModernStatusBarWifiViewTest : SysuiTestCase() {
private lateinit var testableLooper: TestableLooper
@Mock
private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
@Mock
private lateinit var logger: ConnectivityPipelineLogger
@Mock
private lateinit var constants: WifiConstants
private lateinit var connectivityRepository: FakeConnectivityRepository
private lateinit var wifiRepository: FakeWifiRepository
private lateinit var interactor: WifiInteractor
private lateinit var viewModel: WifiViewModel
private lateinit var scope: CoroutineScope
@JvmField @Rule
val instantTaskExecutor = InstantTaskExecutorRule()
@Before
fun setUp() {
Assert.setTestThread(Thread.currentThread())
MockitoAnnotations.initMocks(this)
testableLooper = TestableLooper.get(this)
connectivityRepository = FakeConnectivityRepository()
wifiRepository = FakeWifiRepository()
wifiRepository.setIsWifiEnabled(true)
interactor = WifiInteractor(connectivityRepository, wifiRepository)
scope = CoroutineScope(Dispatchers.Unconfined)
viewModel = WifiViewModel(
constants, context, logger, interactor, scope, statusBarPipelineFlags
)
}
@Test
fun constructAndBind_hasCorrectSlot() {
val view = ModernStatusBarWifiView.constructAndBind(
context, "slotName", mock(), StatusBarLocation.HOME
context, "slotName", viewModel, StatusBarLocation.HOME
)
assertThat(view.slot).isEqualTo("slotName")
}
@Test
fun getVisibleState_icon_returnsIcon() {
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
view.setVisibleState(STATE_ICON, /* animate= */ false)
assertThat(view.visibleState).isEqualTo(STATE_ICON)
}
@Test
fun getVisibleState_dot_returnsDot() {
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
view.setVisibleState(STATE_DOT, /* animate= */ false)
assertThat(view.visibleState).isEqualTo(STATE_DOT)
}
@Test
fun getVisibleState_hidden_returnsHidden() {
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
view.setVisibleState(STATE_HIDDEN, /* animate= */ false)
assertThat(view.visibleState).isEqualTo(STATE_HIDDEN)
}
// Note: The following tests are more like integration tests, since they stand up a full
// [WifiViewModel] and test the interactions between the view, view-binder, and view-model.
@Test
fun setVisibleState_icon_iconShownDotHidden() {
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
view.setVisibleState(STATE_ICON, /* animate= */ false)
ViewUtils.attachView(view)
testableLooper.processAllMessages()
assertThat(view.getIconGroupView().visibility).isEqualTo(View.VISIBLE)
assertThat(view.getDotView().visibility).isEqualTo(View.GONE)
ViewUtils.detachView(view)
}
@Test
fun setVisibleState_dot_iconHiddenDotShown() {
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
view.setVisibleState(STATE_DOT, /* animate= */ false)
ViewUtils.attachView(view)
testableLooper.processAllMessages()
assertThat(view.getIconGroupView().visibility).isEqualTo(View.GONE)
assertThat(view.getDotView().visibility).isEqualTo(View.VISIBLE)
ViewUtils.detachView(view)
}
@Test
fun setVisibleState_hidden_iconAndDotHidden() {
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
view.setVisibleState(STATE_HIDDEN, /* animate= */ false)
ViewUtils.attachView(view)
testableLooper.processAllMessages()
assertThat(view.getIconGroupView().visibility).isEqualTo(View.GONE)
assertThat(view.getDotView().visibility).isEqualTo(View.GONE)
ViewUtils.detachView(view)
}
@Test
fun isIconVisible_notEnabled_outputsFalse() {
wifiRepository.setIsWifiEnabled(false)
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
ViewUtils.attachView(view)
testableLooper.processAllMessages()
assertThat(view.isIconVisible).isFalse()
ViewUtils.detachView(view)
}
@Test
fun isIconVisible_enabled_outputsTrue() {
wifiRepository.setIsWifiEnabled(true)
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
ViewUtils.attachView(view)
testableLooper.processAllMessages()
assertThat(view.isIconVisible).isTrue()
ViewUtils.detachView(view)
}
private fun View.getIconGroupView(): View {
return this.requireViewById(R.id.wifi_group)
}
private fun View.getDotView(): View {
return this.requireViewById(R.id.status_bar_dot)
}
}
private const val SLOT_NAME = "TestSlotName"