Merge "Migrate DataSaverTile" into main
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt
new file mode 100644
index 0000000..4b96251
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.impl.saver.domain
+
+import android.content.SharedPreferences
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.utils.leaks.FakeDataSaverController
+import kotlin.coroutines.EmptyCoroutineContext
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+
+/** Test [DataSaverDialogDelegate]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DataSaverDialogDelegateTest : SysuiTestCase() {
+
+ private val dataSaverController = FakeDataSaverController(LeakCheck())
+
+ private lateinit var sysuiDialogFactory: SystemUIDialog.Factory
+ private lateinit var sysuiDialog: SystemUIDialog
+ private lateinit var dataSaverDialogDelegate: DataSaverDialogDelegate
+
+ @Before
+ fun setup() {
+ sysuiDialog = mock<SystemUIDialog>()
+ sysuiDialogFactory = mock<SystemUIDialog.Factory>()
+
+ dataSaverDialogDelegate =
+ DataSaverDialogDelegate(
+ sysuiDialogFactory,
+ context,
+ EmptyCoroutineContext,
+ dataSaverController,
+ mock<SharedPreferences>()
+ )
+
+ whenever(sysuiDialogFactory.create(eq(dataSaverDialogDelegate), eq(context)))
+ .thenReturn(sysuiDialog)
+ }
+ @Test
+ fun delegateSetsDialogTitleCorrectly() {
+ val expectedResId = R.string.data_saver_enable_title
+
+ dataSaverDialogDelegate.onCreate(sysuiDialog, null)
+
+ verify(sysuiDialog).setTitle(eq(expectedResId))
+ }
+
+ @Test
+ fun delegateSetsDialogMessageCorrectly() {
+ val expectedResId = R.string.data_saver_description
+
+ dataSaverDialogDelegate.onCreate(sysuiDialog, null)
+
+ verify(sysuiDialog).setMessage(expectedResId)
+ }
+
+ @Test
+ fun delegateSetsDialogPositiveButtonCorrectly() {
+ val expectedResId = R.string.data_saver_enable_button
+
+ dataSaverDialogDelegate.onCreate(sysuiDialog, null)
+
+ verify(sysuiDialog).setPositiveButton(eq(expectedResId), any())
+ }
+
+ @Test
+ fun delegateSetsDialogCancelButtonCorrectly() {
+ val expectedResId = R.string.cancel
+
+ dataSaverDialogDelegate.onCreate(sysuiDialog, null)
+
+ verify(sysuiDialog).setNeutralButton(eq(expectedResId), eq(null))
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapperTest.kt
new file mode 100644
index 0000000..d182412
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapperTest.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.impl.saver.domain
+
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.impl.saver.domain.model.DataSaverTileModel
+import com.android.systemui.qs.tiles.impl.saver.qsDataSaverTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DataSaverTileMapperTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val dataSaverTileConfig = kosmos.qsDataSaverTileConfig
+
+ // Using lazy (versus =) to make sure we override the right context -- see b/311612168
+ private val mapper by lazy { DataSaverTileMapper(context.orCreateTestableResources.resources) }
+
+ @Test
+ fun activeStateMatchesEnabledModel() {
+ val inputModel = DataSaverTileModel(true)
+
+ val outputState = mapper.map(dataSaverTileConfig, inputModel)
+
+ val expectedState =
+ createDataSaverTileState(
+ QSTileState.ActivationState.ACTIVE,
+ R.drawable.qs_data_saver_icon_on
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun inactiveStateMatchesDisabledModel() {
+ val inputModel = DataSaverTileModel(false)
+
+ val outputState = mapper.map(dataSaverTileConfig, inputModel)
+
+ val expectedState =
+ createDataSaverTileState(
+ QSTileState.ActivationState.INACTIVE,
+ R.drawable.qs_data_saver_icon_off
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ private fun createDataSaverTileState(
+ activationState: QSTileState.ActivationState,
+ iconRes: Int
+ ): QSTileState {
+ val label = context.getString(R.string.data_saver)
+ val secondaryLabel =
+ if (activationState == QSTileState.ActivationState.ACTIVE)
+ context.resources.getStringArray(R.array.tile_states_saver)[2]
+ else if (activationState == QSTileState.ActivationState.INACTIVE)
+ context.resources.getStringArray(R.array.tile_states_saver)[1]
+ else context.resources.getStringArray(R.array.tile_states_saver)[0]
+
+ return QSTileState(
+ { Icon.Resource(iconRes, null) },
+ label,
+ activationState,
+ secondaryLabel,
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+ label,
+ null,
+ QSTileState.SideViewIcon.None,
+ QSTileState.EnabledState.ENABLED,
+ Switch::class.qualifiedName
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileDataInteractorTest.kt
new file mode 100644
index 0000000..819bd03
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileDataInteractorTest.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.impl.saver.domain.interactor
+
+import android.os.UserHandle
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.impl.saver.domain.model.DataSaverTileModel
+import com.android.systemui.utils.leaks.FakeDataSaverController
+import com.google.common.truth.Truth
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DataSaverTileDataInteractorTest : SysuiTestCase() {
+ private val controller: FakeDataSaverController = FakeDataSaverController(LeakCheck())
+ private val underTest: DataSaverTileDataInteractor = DataSaverTileDataInteractor(controller)
+
+ @Test
+ fun isAvailableRegardlessOfController() = runTest {
+ controller.setDataSaverEnabled(false)
+
+ runCurrent()
+ val availability by collectLastValue(underTest.availability(TEST_USER))
+
+ Truth.assertThat(availability).isTrue()
+ }
+
+ @Test
+ fun dataMatchesController() = runTest {
+ controller.setDataSaverEnabled(false)
+ val flowValues: List<DataSaverTileModel> by
+ collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)))
+
+ runCurrent()
+ controller.setDataSaverEnabled(true)
+ runCurrent()
+ controller.setDataSaverEnabled(false)
+ runCurrent()
+
+ Truth.assertThat(flowValues.size).isEqualTo(3)
+ Truth.assertThat(flowValues.map { it.isEnabled })
+ .containsExactly(false, true, false)
+ .inOrder()
+ }
+
+ private companion object {
+ val TEST_USER = UserHandle.of(1)!!
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractorTest.kt
new file mode 100644
index 0000000..7091cb3
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractorTest.kt
@@ -0,0 +1,183 @@
+/*
+ * 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.impl.saver.domain.interactor
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.provider.Settings
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.intentInputs
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.impl.saver.domain.model.DataSaverTileModel
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.utils.leaks.FakeDataSaverController
+import com.google.common.truth.Truth.assertThat
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DataSaverTileUserActionInteractorTest : SysuiTestCase() {
+ private val qsTileIntentUserActionHandler = FakeQSTileIntentUserInputHandler()
+ private val dataSaverController = FakeDataSaverController(LeakCheck())
+
+ private lateinit var userFileManager: UserFileManager
+ private lateinit var sharedPreferences: SharedPreferences
+ private lateinit var dialogFactory: SystemUIDialog.Factory
+ private lateinit var underTest: DataSaverTileUserActionInteractor
+
+ @Before
+ fun setup() {
+ userFileManager = mock<UserFileManager>()
+ sharedPreferences = mock<SharedPreferences>()
+ dialogFactory = mock<SystemUIDialog.Factory>()
+ whenever(
+ userFileManager.getSharedPreferences(
+ eq(DataSaverTileUserActionInteractor.PREFS),
+ eq(Context.MODE_PRIVATE),
+ eq(context.userId)
+ )
+ )
+ .thenReturn(sharedPreferences)
+
+ underTest =
+ DataSaverTileUserActionInteractor(
+ context,
+ EmptyCoroutineContext,
+ EmptyCoroutineContext,
+ dataSaverController,
+ qsTileIntentUserActionHandler,
+ mock<DialogLaunchAnimator>(),
+ dialogFactory,
+ userFileManager,
+ )
+ }
+
+ /** Since the dialog was shown before, we expect the click to enable the controller. */
+ @Test
+ fun handleClickToEnableDialogShownBefore() = runTest {
+ whenever(
+ sharedPreferences.getBoolean(
+ eq(DataSaverTileUserActionInteractor.DIALOG_SHOWN),
+ any()
+ )
+ )
+ .thenReturn(true)
+ val stateBeforeClick = false
+
+ underTest.handleInput(QSTileInputTestKtx.click(DataSaverTileModel(stateBeforeClick)))
+
+ assertThat(dataSaverController.isDataSaverEnabled).isEqualTo(!stateBeforeClick)
+ }
+
+ /**
+ * The first time the tile is clicked to turn on we expect (1) the enabled state to not change
+ * and (2) the dialog to be shown instead.
+ */
+ @Test
+ fun handleClickToEnableDialogNotShownBefore() = runTest {
+ whenever(
+ sharedPreferences.getBoolean(
+ eq(DataSaverTileUserActionInteractor.DIALOG_SHOWN),
+ any()
+ )
+ )
+ .thenReturn(false)
+ val mockDialog = mock<SystemUIDialog>()
+ whenever(dialogFactory.create(any(), any())).thenReturn(mockDialog)
+ val stateBeforeClick = false
+
+ val input = QSTileInputTestKtx.click(DataSaverTileModel(stateBeforeClick))
+ underTest.handleInput(input)
+
+ assertThat(dataSaverController.isDataSaverEnabled).isEqualTo(stateBeforeClick)
+ verify(mockDialog).show()
+ }
+
+ /** Disabling should flip the state, even if the dialog was not shown before. */
+ @Test
+ fun handleClickToDisableDialogNotShownBefore() = runTest {
+ whenever(
+ sharedPreferences.getBoolean(
+ eq(DataSaverTileUserActionInteractor.DIALOG_SHOWN),
+ any()
+ )
+ )
+ .thenReturn(false)
+ val enabledBeforeClick = true
+
+ underTest.handleInput(QSTileInputTestKtx.click(DataSaverTileModel(enabledBeforeClick)))
+
+ assertThat(dataSaverController.isDataSaverEnabled).isEqualTo(!enabledBeforeClick)
+ }
+
+ @Test
+ fun handleClickToDisableDialogShownBefore() = runTest {
+ whenever(
+ sharedPreferences.getBoolean(
+ eq(DataSaverTileUserActionInteractor.DIALOG_SHOWN),
+ any()
+ )
+ )
+ .thenReturn(true)
+ val enabledBeforeClick = true
+
+ underTest.handleInput(QSTileInputTestKtx.click(DataSaverTileModel(enabledBeforeClick)))
+
+ assertThat(dataSaverController.isDataSaverEnabled).isEqualTo(!enabledBeforeClick)
+ }
+
+ @Test
+ fun handleLongClickWhenEnabled() = runTest {
+ val enabledState = true
+
+ underTest.handleInput(QSTileInputTestKtx.longClick(DataSaverTileModel(enabledState)))
+
+ assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1)
+ val intentInput = qsTileIntentUserActionHandler.intentInputs.last()
+ val actualIntentAction = intentInput.intent.action
+ val expectedIntentAction = Settings.ACTION_DATA_SAVER_SETTINGS
+ assertThat(actualIntentAction).isEqualTo(expectedIntentAction)
+ }
+
+ @Test
+ fun handleLongClickWhenDisabled() = runTest {
+ val enabledState = false
+
+ underTest.handleInput(QSTileInputTestKtx.longClick(DataSaverTileModel(enabledState)))
+
+ assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1)
+ val intentInput = qsTileIntentUserActionHandler.intentInputs.last()
+ val actualIntentAction = intentInput.intent.action
+ val expectedIntentAction = Settings.ACTION_DATA_SAVER_SETTINGS
+ assertThat(actualIntentAction).isEqualTo(expectedIntentAction)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt
new file mode 100644
index 0000000..fc42ba4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.impl.saver.domain
+
+import android.content.Context
+import android.content.DialogInterface
+import android.content.SharedPreferences
+import android.os.Bundle
+import com.android.internal.R
+import com.android.systemui.qs.tiles.impl.saver.domain.interactor.DataSaverTileUserActionInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.policy.DataSaverController
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+class DataSaverDialogDelegate(
+ private val sysuiDialogFactory: SystemUIDialog.Factory,
+ private val context: Context,
+ private val backgroundContext: CoroutineContext,
+ private val dataSaverController: DataSaverController,
+ private val sharedPreferences: SharedPreferences,
+) : SystemUIDialog.Delegate {
+ override fun createDialog(): SystemUIDialog {
+ return sysuiDialogFactory.create(this, context)
+ }
+
+ override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+ with(dialog) {
+ setTitle(R.string.data_saver_enable_title)
+ setMessage(R.string.data_saver_description)
+ setPositiveButton(R.string.data_saver_enable_button) { _: DialogInterface?, _ ->
+ CoroutineScope(backgroundContext).launch {
+ dataSaverController.setDataSaverEnabled(true)
+ }
+
+ sharedPreferences
+ .edit()
+ .putBoolean(DataSaverTileUserActionInteractor.DIALOG_SHOWN, true)
+ .apply()
+ }
+ setNeutralButton(R.string.cancel, null)
+ setShowForAllUsers(true)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapper.kt
new file mode 100644
index 0000000..25b0913
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapper.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.impl.saver.domain
+
+import android.content.res.Resources
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.saver.domain.model.DataSaverTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/** Maps [DataSaverTileModel] to [QSTileState]. */
+class DataSaverTileMapper @Inject constructor(@Main private val resources: Resources) :
+ QSTileDataToStateMapper<DataSaverTileModel> {
+ override fun map(config: QSTileConfig, data: DataSaverTileModel): QSTileState =
+ QSTileState.build(resources, config.uiConfig) {
+ with(data) {
+ if (isEnabled) {
+ activationState = QSTileState.ActivationState.ACTIVE
+ icon = { Icon.Resource(R.drawable.qs_data_saver_icon_on, null) }
+ secondaryLabel = resources.getStringArray(R.array.tile_states_saver)[2]
+ } else {
+ activationState = QSTileState.ActivationState.INACTIVE
+ icon = { Icon.Resource(R.drawable.qs_data_saver_icon_off, null) }
+ secondaryLabel = resources.getStringArray(R.array.tile_states_saver)[1]
+ }
+ contentDescription = label
+ supportedActions =
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileDataInteractor.kt
new file mode 100644
index 0000000..91e049b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileDataInteractor.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.impl.saver.domain.interactor
+
+import android.os.UserHandle
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow
+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.saver.domain.model.DataSaverTileModel
+import com.android.systemui.statusbar.policy.DataSaverController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+/** Observes data saver state changes providing the [DataSaverTileModel]. */
+class DataSaverTileDataInteractor
+@Inject
+constructor(
+ private val dataSaverController: DataSaverController,
+) : QSTileDataInteractor<DataSaverTileModel> {
+
+ override fun tileData(
+ user: UserHandle,
+ triggers: Flow<DataUpdateTrigger>
+ ): Flow<DataSaverTileModel> =
+ ConflatedCallbackFlow.conflatedCallbackFlow {
+ val initialValue = dataSaverController.isDataSaverEnabled
+ trySend(DataSaverTileModel(initialValue))
+
+ val callback = DataSaverController.Listener { trySend(DataSaverTileModel(it)) }
+
+ dataSaverController.addCallback(callback)
+ awaitClose { dataSaverController.removeCallback(callback) }
+ }
+
+ override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt
new file mode 100644
index 0000000..af74409
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.impl.saver.domain.interactor
+
+import android.content.Context
+import android.content.Intent
+import android.provider.Settings
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.saver.domain.DataSaverDialogDelegate
+import com.android.systemui.qs.tiles.impl.saver.domain.model.DataSaverTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.policy.DataSaverController
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.withContext
+
+/** Handles data saver tile clicks. */
+class DataSaverTileUserActionInteractor
+@Inject
+constructor(
+ @Application private val context: Context,
+ @Main private val coroutineContext: CoroutineContext,
+ @Background private val backgroundContext: CoroutineContext,
+ private val dataSaverController: DataSaverController,
+ private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+ private val dialogLaunchAnimator: DialogLaunchAnimator,
+ private val systemUIDialogFactory: SystemUIDialog.Factory,
+ userFileManager: UserFileManager,
+) : QSTileUserActionInteractor<DataSaverTileModel> {
+ companion object {
+ private const val INTERACTION_JANK_TAG = "start_data_saver"
+ const val PREFS = "data_saver"
+ const val DIALOG_SHOWN = "data_saver_dialog_shown"
+ }
+
+ val sharedPreferences =
+ userFileManager.getSharedPreferences(PREFS, Context.MODE_PRIVATE, context.userId)
+
+ override suspend fun handleInput(input: QSTileInput<DataSaverTileModel>): Unit =
+ with(input) {
+ when (action) {
+ is QSTileUserAction.Click -> {
+ val wasEnabled: Boolean = data.isEnabled
+ if (wasEnabled || sharedPreferences.getBoolean(DIALOG_SHOWN, false)) {
+ withContext(backgroundContext) {
+ dataSaverController.setDataSaverEnabled(!wasEnabled)
+ }
+ return@with
+ }
+ // Show a dialog to confirm first. Dialogs shown by the DialogLaunchAnimator
+ // must be created and shown on the main thread, so we post it to the UI
+ // handler
+ withContext(coroutineContext) {
+ val dialogContext = action.view?.context ?: context
+ val dialogDelegate =
+ DataSaverDialogDelegate(
+ systemUIDialogFactory,
+ dialogContext,
+ backgroundContext,
+ dataSaverController,
+ sharedPreferences
+ )
+ val dialog = systemUIDialogFactory.create(dialogDelegate, dialogContext)
+
+ if (action.view != null) {
+ dialogLaunchAnimator.showFromView(
+ dialog,
+ action.view!!,
+ DialogCuj(
+ InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+ INTERACTION_JANK_TAG
+ )
+ )
+ } else {
+ dialog.show()
+ }
+ }
+ }
+ is QSTileUserAction.LongClick -> {
+ qsTileIntentUserActionHandler.handle(
+ action.view,
+ Intent(Settings.ACTION_DATA_SAVER_SETTINGS)
+ )
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/model/DataSaverTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/model/DataSaverTileModel.kt
new file mode 100644
index 0000000..040c7bf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/model/DataSaverTileModel.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.impl.saver.domain.model
+
+/**
+ * data saver tile model.
+ *
+ * @param isEnabled is true when the data saver is enabled;
+ */
+@JvmInline value class DataSaverTileModel(val isEnabled: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
index a3adea0..642eacc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
@@ -35,6 +35,10 @@
import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileDataInteractor
import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.airplane.domain.model.AirplaneModeTileModel
+import com.android.systemui.qs.tiles.impl.saver.domain.DataSaverTileMapper
+import com.android.systemui.qs.tiles.impl.saver.domain.interactor.DataSaverTileDataInteractor
+import com.android.systemui.qs.tiles.impl.saver.domain.interactor.DataSaverTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.saver.domain.model.DataSaverTileModel
import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
import com.android.systemui.qs.tiles.viewmodel.QSTilePolicy
import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
@@ -85,6 +89,7 @@
companion object {
const val AIRPLANE_MODE_TILE_SPEC = "airplane"
+ const val DATA_SAVER_TILE_SPEC = "saver"
/** Inject InternetTile or InternetTileNewImpl into tileMap in QSModule */
@Provides
@@ -132,5 +137,36 @@
stateInteractor,
mapper,
)
+
+ @Provides
+ @IntoMap
+ @StringKey(DATA_SAVER_TILE_SPEC)
+ fun provideDataSaverTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+ QSTileConfig(
+ tileSpec = TileSpec.create(DATA_SAVER_TILE_SPEC),
+ uiConfig =
+ QSTileUIConfig.Resource(
+ iconRes = R.drawable.qs_data_saver_icon_off,
+ labelRes = R.string.data_saver,
+ ),
+ instanceId = uiEventLogger.getNewInstanceId(),
+ )
+
+ /** Inject DataSaverTile into tileViewModelMap in QSModule */
+ @Provides
+ @IntoMap
+ @StringKey(DATA_SAVER_TILE_SPEC)
+ fun provideDataSaverTileViewModel(
+ factory: QSTileViewModelFactory.Static<DataSaverTileModel>,
+ mapper: DataSaverTileMapper,
+ stateInteractor: DataSaverTileDataInteractor,
+ userActionInteractor: DataSaverTileUserActionInteractor
+ ): QSTileViewModel =
+ factory.create(
+ TileSpec.create(DATA_SAVER_TILE_SPEC),
+ userActionInteractor,
+ stateInteractor,
+ mapper,
+ )
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/saver/DataSaverTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/saver/DataSaverTileKosmos.kt
new file mode 100644
index 0000000..e9a394a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/saver/DataSaverTileKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.impl.saver
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+import com.android.systemui.statusbar.connectivity.ConnectivityModule
+
+val Kosmos.qsDataSaverTileConfig by
+ Kosmos.Fixture { ConnectivityModule.provideDataSaverTileConfig(qsEventLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeDataSaverController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeDataSaverController.java
index 886722e..bade848 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeDataSaverController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeDataSaverController.java
@@ -19,19 +19,38 @@
import com.android.systemui.statusbar.policy.DataSaverController;
import com.android.systemui.statusbar.policy.DataSaverController.Listener;
+import java.util.ArrayList;
+import java.util.List;
+
public class FakeDataSaverController extends BaseLeakChecker<Listener> implements DataSaverController {
+ private boolean mIsEnabled = false;
+ private List<Listener> mListeners = new ArrayList<>();
+
public FakeDataSaverController(LeakCheck test) {
super(test, "datasaver");
}
@Override
public boolean isDataSaverEnabled() {
- return false;
+ return mIsEnabled;
}
@Override
public void setDataSaverEnabled(boolean enabled) {
+ mIsEnabled = enabled;
+ for (Listener listener: mListeners) {
+ listener.onDataSaverChanged(enabled);
+ }
+ }
+ @Override
+ public void addCallback(Listener listener) {
+ mListeners.add(listener);
+ }
+
+ @Override
+ public void removeCallback(Listener listener) {
+ mListeners.remove(listener);
}
}