diff options
| author | 2024-09-09 23:52:50 +0000 | |
|---|---|---|
| committer | 2024-09-26 18:07:44 +0000 | |
| commit | 3af495a12662bd32ceab4757b4c6f37da85738a8 (patch) | |
| tree | 600bf2f8d4c6752eae692e2bd8cf356e18641017 | |
| parent | 6d2364451564e0f3f837b72d7d0ba6c5b9cc7277 (diff) | |
Deduplicate internet tile cellular icon loading
When loading the mobile internet icon, we updated icon multiple times
for the same icon, because the SignalDrawables were Loaded icons and
the equality of SignalDrawables was done by comparing their references.
This CL compares the drawables by comparing their (signal) level. Which
should lead to handleStateChanged being called less frequently when
opening the shade, hence fewers frames dropping on that CUJ.
The wifi and cellular icons will be loaded in the mapper, rather than
the data interactor, which should relieve the Background thread and
hence reduce thread contention.
Finally, a Handler can be injected into SignalDrawable now, which
removes an edge case in our mapper test for cellular drawable. Not
creating a handler on the test thread means we no longer need to make
that test run with looper.
Bug: 361558581
Flag: com.android.systemui.qs_new_tiles
Test: manual: run systemui-quicksettings-toggle-jank-suite verify there
is no performance regression
Test: atest InternetTileMapperTest InternetTileDataInteractorTest
Change-Id: I6e4cac667399941029caafe0b8469639c434a8c0
7 files changed, 238 insertions, 160 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java index ef0f6cbc6ed9..13a06017abbc 100644 --- a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java +++ b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java @@ -42,6 +42,8 @@ import androidx.annotation.Nullable; import com.android.settingslib.R; import com.android.settingslib.Utils; +import java.util.Objects; + /** * Drawable displaying a mobile cell signal indicator. */ @@ -90,6 +92,10 @@ public class SignalDrawable extends DrawableWrapper { private int mCurrentDot; public SignalDrawable(Context context) { + this(context, new Handler()); + } + + public SignalDrawable(@NonNull Context context, @NonNull Handler handler) { super(context.getDrawable(ICON_RES)); final String attributionPathString = context.getString( com.android.internal.R.string.config_signalAttributionPath); @@ -106,7 +112,7 @@ public class SignalDrawable extends DrawableWrapper { mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size); mTransparentPaint.setColor(context.getColor(android.R.color.transparent)); mTransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); - mHandler = new Handler(); + mHandler = handler; setDarkIntensity(0); } @@ -304,6 +310,17 @@ public class SignalDrawable extends DrawableWrapper { | level; } + @Override + public boolean equals(@Nullable Object other) { + return other instanceof SignalDrawable + && ((SignalDrawable) other).getLevel() == this.getLevel(); + } + + @Override + public int hashCode() { + return Objects.hash(getLevel()); + } + /** Returns the state representing empty mobile signal with the given number of levels. */ public static int getEmptyState(int numLevels) { return getState(0, numLevels, true); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt index 620e90dcaa62..d32ba47204c0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt @@ -17,13 +17,17 @@ package com.android.systemui.qs.tiles.impl.internet.domain import android.graphics.drawable.TestStubDrawable +import android.os.fakeExecutorHandler import android.widget.Switch import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.settingslib.graph.SignalDrawable import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text +import com.android.systemui.common.shared.model.Text.Companion.loadText import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel @@ -31,6 +35,9 @@ import com.android.systemui.qs.tiles.impl.internet.qsInternetTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS +import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel +import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel +import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel import org.junit.Test import org.junit.runner.RunWith @@ -39,25 +46,93 @@ import org.junit.runner.RunWith class InternetTileMapperTest : SysuiTestCase() { private val kosmos = Kosmos() private val internetTileConfig = kosmos.qsInternetTileConfig + private val handler = kosmos.fakeExecutorHandler private val mapper by lazy { InternetTileMapper( context.orCreateTestableResources .apply { addOverride(R.drawable.ic_qs_no_internet_unavailable, TestStubDrawable()) + addOverride(R.drawable.ic_satellite_connected_2, TestStubDrawable()) addOverride(wifiRes, TestStubDrawable()) } .resources, context.theme, - context + context, + handler, ) } @Test - fun withActiveModel_mappedStateMatchesDataModel() { + fun withActiveCellularModel_mappedStateMatchesDataModel() { val inputModel = InternetTileModel.Active( secondaryLabel = Text.Resource(R.string.quick_settings_networks_available), - iconId = wifiRes, + icon = InternetTileIconModel.Cellular(3), + stateDescription = null, + contentDescription = + ContentDescription.Resource(R.string.quick_settings_internet_label), + ) + + val outputState = mapper.map(internetTileConfig, inputModel) + + val signalDrawable = SignalDrawable(context, handler) + signalDrawable.setLevel(3) + val expectedState = + createInternetTileState( + QSTileState.ActivationState.ACTIVE, + context.getString(R.string.quick_settings_networks_available), + Icon.Loaded(signalDrawable, null), + null, + context.getString(R.string.quick_settings_internet_label), + ) + QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) + } + + @Test + fun withActiveSatelliteModel_mappedStateMatchesDataModel() { + val inputIcon = + SignalIconModel.Satellite( + 3, + Icon.Resource( + res = R.drawable.ic_satellite_connected_2, + contentDescription = + ContentDescription.Resource( + R.string.accessibility_status_bar_satellite_good_connection + ), + ), + ) + val inputModel = + InternetTileModel.Active( + secondaryLabel = Text.Resource(R.string.quick_settings_networks_available), + icon = InternetTileIconModel.Satellite(inputIcon.icon), + stateDescription = null, + contentDescription = + ContentDescription.Resource( + R.string.accessibility_status_bar_satellite_good_connection + ), + ) + + val outputState = mapper.map(internetTileConfig, inputModel) + + val expectedSatIcon = SatelliteIconModel.fromSignalStrength(3) + + val expectedState = + createInternetTileState( + QSTileState.ActivationState.ACTIVE, + inputModel.secondaryLabel.loadText(context).toString(), + Icon.Loaded(context.getDrawable(expectedSatIcon!!.res)!!, null), + expectedSatIcon.res, + expectedSatIcon.contentDescription.loadContentDescription(context).toString(), + ) + QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) + } + + @Test + fun withActiveWifiModel_mappedStateMatchesDataModel() { + val inputModel = + InternetTileModel.Active( + secondaryLabel = Text.Resource(R.string.quick_settings_networks_available), + icon = InternetTileIconModel.ResourceId(wifiRes), stateDescription = null, contentDescription = ContentDescription.Resource(R.string.quick_settings_internet_label), @@ -71,7 +146,7 @@ class InternetTileMapperTest : SysuiTestCase() { context.getString(R.string.quick_settings_networks_available), Icon.Loaded(context.getDrawable(wifiRes)!!, contentDescription = null), wifiRes, - context.getString(R.string.quick_settings_internet_label) + context.getString(R.string.quick_settings_internet_label), ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } @@ -81,7 +156,7 @@ class InternetTileMapperTest : SysuiTestCase() { val inputModel = InternetTileModel.Inactive( secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable), - iconId = R.drawable.ic_qs_no_internet_unavailable, + icon = InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable), stateDescription = null, contentDescription = ContentDescription.Resource(R.string.quick_settings_networks_unavailable), @@ -95,10 +170,10 @@ class InternetTileMapperTest : SysuiTestCase() { context.getString(R.string.quick_settings_networks_unavailable), Icon.Loaded( context.getDrawable(R.drawable.ic_qs_no_internet_unavailable)!!, - contentDescription = null + contentDescription = null, ), R.drawable.ic_qs_no_internet_unavailable, - context.getString(R.string.quick_settings_networks_unavailable) + context.getString(R.string.quick_settings_networks_unavailable), ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } @@ -107,7 +182,7 @@ class InternetTileMapperTest : SysuiTestCase() { activationState: QSTileState.ActivationState, secondaryLabel: String, icon: Icon, - iconRes: Int, + iconRes: Int? = null, contentDescription: String, ): QSTileState { val label = context.getString(R.string.quick_settings_internet_label) @@ -120,13 +195,13 @@ class InternetTileMapperTest : SysuiTestCase() { setOf( QSTileState.UserAction.CLICK, QSTileState.UserAction.TOGGLE_CLICK, - QSTileState.UserAction.LONG_CLICK + QSTileState.UserAction.LONG_CLICK, ), contentDescription, null, QSTileState.SideViewIcon.Chevron, QSTileState.EnabledState.ENABLED, - Switch::class.qualifiedName + Switch::class.qualifiedName, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt index 5a4506086058..5259aa84b193 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt @@ -18,14 +18,12 @@ package com.android.systemui.qs.tiles.impl.internet.domain.interactor import android.graphics.drawable.TestStubDrawable import android.os.UserHandle -import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.AccessibilityContentDescriptions import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription -import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.common.shared.model.Text.Companion.loadText import com.android.systemui.coroutines.collectLastValue @@ -49,6 +47,7 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIc import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel @@ -60,9 +59,7 @@ import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -import org.junit.Assume.assumeFalse import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -144,7 +141,6 @@ class InternetTileDataInteractorTest : SysuiTestCase() { underTest = InternetTileDataInteractor( context, - testScope.coroutineContext, testScope.backgroundScope, airplaneModeRepository, connectivityRepository, @@ -164,9 +160,11 @@ class InternetTileDataInteractorTest : SysuiTestCase() { connectivityRepository.defaultConnections.value = DefaultConnectionModel() + val expectedIcon = + InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable) assertThat(latest?.secondaryLabel) .isEqualTo(Text.Resource(R.string.quick_settings_networks_unavailable)) - assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_unavailable) + assertThat(latest?.icon).isEqualTo(expectedIcon) } @Test @@ -183,11 +181,8 @@ class InternetTileDataInteractorTest : SysuiTestCase() { underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)) ) - val networkModel = - WifiNetworkModel.Active.of( - level = 4, - ssid = "test ssid", - ) + val networkModel = WifiNetworkModel.Active.of(level = 4, ssid = "test ssid") + val wifiIcon = WifiIcon.fromModel(model = networkModel, context = context, showHotspotInfo = true) as WifiIcon.Visible @@ -198,12 +193,9 @@ class InternetTileDataInteractorTest : SysuiTestCase() { assertThat(latest?.secondaryTitle).isEqualTo("test ssid") assertThat(latest?.secondaryLabel).isNull() - val expectedIcon = - Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null) - val actualIcon = latest?.icon - assertThat(actualIcon).isEqualTo(expectedIcon) - assertThat(latest?.iconId).isEqualTo(WifiIcons.WIFI_NO_INTERNET_ICONS[4]) + val expectedIcon = InternetTileIconModel.ResourceId(WifiIcons.WIFI_NO_INTERNET_ICONS[4]) + assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.contentDescription.loadContentDescription(context)) .isEqualTo("$internet,test ssid") val expectedSd = wifiIcon.contentDescription @@ -229,8 +221,7 @@ class InternetTileDataInteractorTest : SysuiTestCase() { wifiRepository.setIsWifiDefault(true) wifiRepository.setWifiNetwork(networkModel) - val expectedIcon = - Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null) + val expectedIcon = InternetTileIconModel.ResourceId(WifiIcons.WIFI_NO_INTERNET_ICONS[4]) assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) .doesNotContain( @@ -249,9 +240,8 @@ class InternetTileDataInteractorTest : SysuiTestCase() { setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.TABLET) val expectedIcon = - Icon.Loaded( - context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_tablet)!!, - null + InternetTileIconModel.ResourceId( + com.android.settingslib.R.drawable.ic_hotspot_tablet ) assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) @@ -271,9 +261,8 @@ class InternetTileDataInteractorTest : SysuiTestCase() { setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.LAPTOP) val expectedIcon = - Icon.Loaded( - context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_laptop)!!, - null + InternetTileIconModel.ResourceId( + com.android.settingslib.R.drawable.ic_hotspot_laptop ) assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) @@ -293,10 +282,10 @@ class InternetTileDataInteractorTest : SysuiTestCase() { setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.WATCH) val expectedIcon = - Icon.Loaded( - context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_watch)!!, - null + InternetTileIconModel.ResourceId( + com.android.settingslib.R.drawable.ic_hotspot_watch ) + assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) .isEqualTo( @@ -315,10 +304,7 @@ class InternetTileDataInteractorTest : SysuiTestCase() { setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.AUTO) val expectedIcon = - Icon.Loaded( - context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_auto)!!, - null - ) + InternetTileIconModel.ResourceId(com.android.settingslib.R.drawable.ic_hotspot_auto) assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) .isEqualTo( @@ -336,9 +322,8 @@ class InternetTileDataInteractorTest : SysuiTestCase() { setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.PHONE) val expectedIcon = - Icon.Loaded( - context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!, - null + InternetTileIconModel.ResourceId( + com.android.settingslib.R.drawable.ic_hotspot_phone ) assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) @@ -358,9 +343,8 @@ class InternetTileDataInteractorTest : SysuiTestCase() { setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.UNKNOWN) val expectedIcon = - Icon.Loaded( - context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!, - null + InternetTileIconModel.ResourceId( + com.android.settingslib.R.drawable.ic_hotspot_phone ) assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) @@ -380,10 +364,10 @@ class InternetTileDataInteractorTest : SysuiTestCase() { setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.INVALID) val expectedIcon = - Icon.Loaded( - context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!, - null + InternetTileIconModel.ResourceId( + com.android.settingslib.R.drawable.ic_hotspot_phone ) + assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) .isEqualTo( @@ -426,8 +410,9 @@ class InternetTileDataInteractorTest : SysuiTestCase() { assertThat(latest?.secondaryLabel).isNull() assertThat(latest?.secondaryTitle) .isEqualTo(context.getString(R.string.quick_settings_networks_available)) - assertThat(latest?.icon).isNull() - assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_available) + val expectedIcon = + InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_available) + assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription).isNull() val expectedCd = "$internet,${context.getString(R.string.quick_settings_networks_available)}" @@ -435,54 +420,19 @@ class InternetTileDataInteractorTest : SysuiTestCase() { .isEqualTo(expectedCd) } - /** - * We expect a RuntimeException because [underTest] instantiates a SignalDrawable on the - * provided context, and so the SignalDrawable constructor attempts to instantiate a Handler() - * on the mentioned context. Since that context does not have a looper assigned to it, the - * handler instantiation will throw a RuntimeException. - * - * TODO(b/338068066): Robolectric behavior differs in that it does not throw the exception So - * either we should make Robolectric behave similar to the device test, or change this test to - * look for a different signal than the exception, when run by Robolectric. For now we just - * assume the test is not Robolectric. - */ - @Test(expected = java.lang.RuntimeException::class) - fun mobileDefault_usesNetworkNameAndIcon_throwsRunTimeException() = - testScope.runTest { - assumeFalse(isRobolectricTest()) - - collectLastValue(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))) - - connectivityRepository.setMobileConnected() - mobileConnectionsRepository.mobileIsDefault.value = true - mobileConnectionRepository.apply { - setAllLevels(3) - setAllRoaming(false) - networkName.value = NetworkNameModel.Default("test network") - } - - runCurrent() - } - - /** - * See [mobileDefault_usesNetworkNameAndIcon_throwsRunTimeException] for description of the - * problem this test solves. The solution here is to assign a looper to the context via - * RunWithLooper. In the production code, the solution is to use a Main CoroutineContext for - * creating the SignalDrawable. - */ - @TestableLooper.RunWithLooper @Test - fun mobileDefault_run_withLooper_usesNetworkNameAndIcon() = + fun mobileDefault_usesNetworkNameAndIcon() = testScope.runTest { val latest by collectLastValue( underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)) ) + val iconLevel = 3 connectivityRepository.setMobileConnected() mobileConnectionsRepository.mobileIsDefault.value = true mobileConnectionRepository.apply { - setAllLevels(3) + setAllLevels(iconLevel) setAllRoaming(false) networkName.value = NetworkNameModel.Default("test network") } @@ -491,8 +441,9 @@ class InternetTileDataInteractorTest : SysuiTestCase() { assertThat(latest?.secondaryTitle).isNotNull() assertThat(latest?.secondaryTitle.toString()).contains("test network") assertThat(latest?.secondaryLabel).isNull() - assertThat(latest?.icon).isInstanceOf(Icon.Loaded::class.java) - assertThat(latest?.iconId).isNull() + val expectedIcon = InternetTileIconModel.Cellular(iconLevel) + + assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) .isEqualTo(latest?.secondaryTitle.toString()) assertThat(latest?.contentDescription.loadContentDescription(context)) @@ -513,8 +464,8 @@ class InternetTileDataInteractorTest : SysuiTestCase() { assertThat(latest?.secondaryLabel.loadText(context)) .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context)) assertThat(latest?.secondaryTitle).isNull() - assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet_fully) - assertThat(latest?.icon).isNull() + val expectedIcon = InternetTileIconModel.ResourceId(R.drawable.stat_sys_ethernet_fully) + assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription).isNull() assertThat(latest?.contentDescription.loadContentDescription(context)) .isEqualTo(latest?.secondaryLabel.loadText(context)) @@ -534,8 +485,8 @@ class InternetTileDataInteractorTest : SysuiTestCase() { assertThat(latest?.secondaryLabel.loadText(context)) .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context)) assertThat(latest?.secondaryTitle).isNull() - assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet) - assertThat(latest?.icon).isNull() + val expectedIcon = InternetTileIconModel.ResourceId(R.drawable.stat_sys_ethernet) + assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription).isNull() assertThat(latest?.contentDescription.loadContentDescription(context)) .isEqualTo(latest?.secondaryLabel.loadText(context)) @@ -543,11 +494,7 @@ class InternetTileDataInteractorTest : SysuiTestCase() { private fun setWifiNetworkWithHotspot(hotspot: WifiNetworkModel.HotspotDeviceType) { val networkModel = - WifiNetworkModel.Active.of( - level = 4, - ssid = "test ssid", - hotspotDeviceType = hotspot, - ) + WifiNetworkModel.Active.of(level = 4, ssid = "test ssid", hotspotDeviceType = hotspot) connectivityRepository.setWifiConnected() wifiRepository.setIsWifiDefault(true) @@ -560,7 +507,7 @@ class InternetTileDataInteractorTest : SysuiTestCase() { val NOT_CONNECTED_NETWORKS_UNAVAILABLE = InternetTileModel.Inactive( secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable), - iconId = R.drawable.ic_qs_no_internet_unavailable, + icon = InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable), stateDescription = null, contentDescription = ContentDescription.Resource(R.string.quick_settings_networks_unavailable), diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt index 8965ef2bc493..bb0b9b7084fa 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt @@ -18,7 +18,9 @@ package com.android.systemui.qs.tiles.impl.internet.domain import android.content.Context import android.content.res.Resources +import android.os.Handler import android.widget.Switch +import com.android.settingslib.graph.SignalDrawable import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text.Companion.loadText @@ -28,6 +30,7 @@ import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileMode import com.android.systemui.qs.tiles.viewmodel.QSTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R +import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel import javax.inject.Inject /** Maps [InternetTileModel] to [QSTileState]. */ @@ -37,6 +40,7 @@ constructor( @Main private val resources: Resources, private val theme: Resources.Theme, private val context: Context, + @Main private val handler: Handler, ) : QSTileDataToStateMapper<InternetTileModel> { override fun map(config: QSTileConfig, data: InternetTileModel): QSTileState = @@ -44,25 +48,42 @@ constructor( label = resources.getString(R.string.quick_settings_internet_label) expandedAccessibilityClass = Switch::class - if (data.secondaryLabel != null) { - secondaryLabel = data.secondaryLabel.loadText(context) - } else { - secondaryLabel = data.secondaryTitle - } + secondaryLabel = + if (data.secondaryLabel != null) { + data.secondaryLabel.loadText(context) + } else { + data.secondaryTitle + } stateDescription = data.stateDescription.loadContentDescription(context) contentDescription = data.contentDescription.loadContentDescription(context) - iconRes = data.iconId - if (data.icon != null) { - this.icon = { data.icon } - } else if (data.iconId != null) { - val loadedIcon = - Icon.Loaded( - resources.getDrawable(data.iconId!!, theme), - contentDescription = null - ) - this.icon = { loadedIcon } + when (val dataIcon = data.icon) { + is InternetTileIconModel.ResourceId -> { + iconRes = dataIcon.resId + icon = { + Icon.Loaded( + resources.getDrawable(dataIcon.resId, theme), + contentDescription = null, + ) + } + } + + is InternetTileIconModel.Cellular -> { + val signalDrawable = SignalDrawable(context, handler) + signalDrawable.setLevel(dataIcon.level) + icon = { Icon.Loaded(signalDrawable, contentDescription = null) } + } + + is InternetTileIconModel.Satellite -> { + iconRes = dataIcon.resourceIcon.res // level is inferred from res + icon = { + Icon.Loaded( + resources.getDrawable(dataIcon.resourceIcon.res, theme), + contentDescription = null, + ) + } + } } sideViewIcon = QSTileState.SideViewIcon.Chevron @@ -75,7 +96,7 @@ constructor( setOf( QSTileState.UserAction.CLICK, QSTileState.UserAction.TOGGLE_CLICK, - QSTileState.UserAction.LONG_CLICK + QSTileState.UserAction.LONG_CLICK, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt index 204ead3fe29c..6fe3979fa446 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt @@ -20,13 +20,10 @@ import android.annotation.StringRes import android.content.Context import android.os.UserHandle import android.text.Html -import com.android.settingslib.graph.SignalDrawable import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription -import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel @@ -36,12 +33,12 @@ import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteracto import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository +import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon import com.android.systemui.utils.coroutines.flow.mapLatestConflated import javax.inject.Inject -import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -51,7 +48,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.withContext @OptIn(ExperimentalCoroutinesApi::class) /** Observes internet state changes providing the [InternetTileModel]. */ @@ -59,7 +55,6 @@ class InternetTileDataInteractor @Inject constructor( private val context: Context, - @Main private val mainCoroutineContext: CoroutineContext, @Application private val scope: CoroutineScope, airplaneModeRepository: AirplaneModeRepository, private val connectivityRepository: ConnectivityRepository, @@ -79,8 +74,7 @@ constructor( flowOf( InternetTileModel.Active( secondaryTitle = secondary, - iconId = wifiIcon.icon.res, - icon = Icon.Loaded(context.getDrawable(wifiIcon.icon.res)!!, null), + icon = InternetTileIconModel.ResourceId(wifiIcon.icon.res), stateDescription = wifiIcon.contentDescription, contentDescription = ContentDescription.Loaded("$internetLabel,$secondary"), ) @@ -116,11 +110,10 @@ constructor( if (it == null) { notConnectedFlow } else { - combine( - it.networkName, - it.signalLevelIcon, - mobileDataContentName, - ) { networkNameModel, signalIcon, dataContentDescription -> + combine(it.networkName, it.signalLevelIcon, mobileDataContentName) { + networkNameModel, + signalIcon, + dataContentDescription -> Triple(networkNameModel, signalIcon, dataContentDescription) } .mapLatestConflated { (networkNameModel, signalIcon, dataContentDescription) -> @@ -129,17 +122,12 @@ constructor( val secondary = mobileDataContentConcat( networkNameModel.name, - dataContentDescription + dataContentDescription, ) - val drawable = - withContext(mainCoroutineContext) { SignalDrawable(context) } - drawable.setLevel(signalIcon.level) - val loadedIcon = Icon.Loaded(drawable, null) - InternetTileModel.Active( secondaryTitle = secondary, - icon = loadedIcon, + icon = InternetTileIconModel.Cellular(signalIcon.level), stateDescription = ContentDescription.Loaded(secondary.toString()), contentDescription = ContentDescription.Loaded(internetLabel), @@ -150,9 +138,10 @@ constructor( signalIcon.icon.contentDescription.loadContentDescription( context ) + InternetTileModel.Active( secondaryTitle = secondary, - iconId = signalIcon.icon.res, + icon = InternetTileIconModel.Satellite(signalIcon.icon), stateDescription = ContentDescription.Loaded(secondary), contentDescription = ContentDescription.Loaded(internetLabel), ) @@ -164,7 +153,7 @@ constructor( private fun mobileDataContentConcat( networkName: String?, - dataContentDescription: CharSequence? + dataContentDescription: CharSequence?, ): CharSequence { if (dataContentDescription == null) { return networkName ?: "" @@ -177,9 +166,9 @@ constructor( context.getString( R.string.mobile_carrier_text_format, networkName, - dataContentDescription + dataContentDescription, ), - 0 + 0, ) } @@ -199,7 +188,7 @@ constructor( flowOf( InternetTileModel.Active( secondaryLabel = secondary?.toText(), - iconId = it.res, + icon = InternetTileIconModel.ResourceId(it.res), stateDescription = null, contentDescription = secondary, ) @@ -208,16 +197,18 @@ constructor( } private val notConnectedFlow: StateFlow<InternetTileModel> = - combine( - wifiInteractor.areNetworksAvailable, - airplaneModeRepository.isAirplaneMode, - ) { networksAvailable, isAirplaneMode -> + combine(wifiInteractor.areNetworksAvailable, airplaneModeRepository.isAirplaneMode) { + networksAvailable, + isAirplaneMode -> when { isAirplaneMode -> { val secondary = context.getString(R.string.status_bar_airplane) InternetTileModel.Inactive( secondaryTitle = secondary, - iconId = R.drawable.ic_qs_no_internet_unavailable, + icon = + InternetTileIconModel.ResourceId( + R.drawable.ic_qs_no_internet_unavailable + ), stateDescription = null, contentDescription = ContentDescription.Loaded(secondary), ) @@ -227,10 +218,13 @@ constructor( context.getString(R.string.quick_settings_networks_available) InternetTileModel.Inactive( secondaryTitle = secondary, - iconId = R.drawable.ic_qs_no_internet_available, + icon = + InternetTileIconModel.ResourceId( + R.drawable.ic_qs_no_internet_available + ), stateDescription = null, contentDescription = - ContentDescription.Loaded("$internetLabel,$secondary") + ContentDescription.Loaded("$internetLabel,$secondary"), ) } else -> { @@ -248,7 +242,7 @@ constructor( */ override fun tileData( user: UserHandle, - triggers: Flow<DataUpdateTrigger> + triggers: Flow<DataUpdateTrigger>, ): Flow<InternetTileModel> = connectivityRepository.defaultConnections.flatMapLatest { when { @@ -265,7 +259,7 @@ constructor( val NOT_CONNECTED_NETWORKS_UNAVAILABLE = InternetTileModel.Inactive( secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable), - iconId = R.drawable.ic_qs_no_internet_unavailable, + icon = InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable), stateDescription = null, contentDescription = ContentDescription.Resource(R.string.quick_settings_networks_unavailable), diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt index ece904611782..15b4e472eec7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt @@ -17,23 +17,21 @@ package com.android.systemui.qs.tiles.impl.internet.domain.model import com.android.systemui.common.shared.model.ContentDescription -import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text +import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel /** Model describing the state that the QS Internet tile should be in. */ sealed interface InternetTileModel { val secondaryTitle: CharSequence? val secondaryLabel: Text? - val iconId: Int? - val icon: Icon? + val icon: InternetTileIconModel val stateDescription: ContentDescription? val contentDescription: ContentDescription? data class Active( override val secondaryTitle: CharSequence? = null, override val secondaryLabel: Text? = null, - override val iconId: Int? = null, - override val icon: Icon? = null, + override val icon: InternetTileIconModel = InternetTileIconModel.Cellular(1), override val stateDescription: ContentDescription? = null, override val contentDescription: ContentDescription? = null, ) : InternetTileModel @@ -41,8 +39,7 @@ sealed interface InternetTileModel { data class Inactive( override val secondaryTitle: CharSequence? = null, override val secondaryLabel: Text? = null, - override val iconId: Int? = null, - override val icon: Icon? = null, + override val icon: InternetTileIconModel = InternetTileIconModel.Cellular(1), override val stateDescription: ContentDescription? = null, override val contentDescription: ContentDescription? = null, ) : InternetTileModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileIconModel.kt new file mode 100644 index 000000000000..f8958e0d002f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileIconModel.kt @@ -0,0 +1,27 @@ +/* + * 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.statusbar.pipeline.shared.ui.model + +import com.android.systemui.common.shared.model.Icon + +sealed interface InternetTileIconModel { + data class ResourceId(val resId: Int) : InternetTileIconModel + + data class Cellular(val level: Int) : InternetTileIconModel + + data class Satellite(val resourceIcon: Icon.Resource) : InternetTileIconModel +} |