diff options
| author | 2023-12-08 23:12:12 +0000 | |
|---|---|---|
| committer | 2023-12-08 23:12:12 +0000 | |
| commit | 09802a75b7e92d7c7a0a0816d59e98ba660f2b7c (patch) | |
| tree | cd29fd4d22485c750587f36ff67db57ee38cf275 | |
| parent | 93ecf5941a3b2e38b08c6c33b5c57ce346c6a3dc (diff) | |
| parent | f5bbf836a25c4aa273395ce75eb4a2b549c13702 (diff) | |
Merge "Migrate DataSaverTile" into main
12 files changed, 830 insertions, 1 deletions
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 000000000000..4b9625107745 --- /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 000000000000..d1824129590b --- /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 000000000000..819bd03437f4 --- /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 000000000000..7091cb3b259c --- /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 000000000000..fc42ba495a51 --- /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 000000000000..25b09131522b --- /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 000000000000..91e049b68c06 --- /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 000000000000..af74409630ca --- /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 000000000000..040c7bf55236 --- /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 a3adea0b86d9..642eaccc3c99 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.AirplaneModeMapper 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 @@ interface ConnectivityModule { 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 @@ interface ConnectivityModule { 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 000000000000..e9a394aef6de --- /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 886722e46376..bade84890f41 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 android.testing.LeakCheck; 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); } } |