summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Anton Potapov <apotapov@google.com> 2024-10-04 15:20:41 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-10-04 15:20:41 +0000
commitb2be7ebe535ff2349a10c48dd191d3b38436d84c (patch)
tree336884fc9cbd28d9fe854c21c521a0a6f0e30888
parentfa5e4152054e1197a97f640c4f959c3eef91e652 (diff)
parent584582dd7b25d50515ae5664bb596ead2cba911f (diff)
Merge changes Iad89f93d,I6bcc80e1,I43e2245d into main
* changes: Add tests for the dialog showing logic Add showing the new Volume Dialog based on the VolumeDialogController callback Add window configuration for the new Volume Dialog
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt150
-rw-r--r--packages/SystemUI/res/values/styles.xml1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerExt.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt67
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt102
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt48
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt46
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt86
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt92
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt93
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt195
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/plugins/VolumeDialogControllerKosmos.kt24
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt32
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt (renamed from packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt)21
20 files changed, 949 insertions, 119 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt
new file mode 100644
index 000000000000..7ce421a5aa62
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.volume.dialog.domain.interactor
+
+import android.app.ActivityManager
+import android.testing.TestableLooper
+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.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.fakeVolumeDialogController
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogVisibilityModel
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.days
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private val dialogTimeoutDuration = 3.seconds
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper()
+class VolumeDialogVisibilityInteractorTest : SysuiTestCase() {
+
+ private val kosmos: Kosmos = testKosmos()
+
+ private lateinit var underTest: VolumeDialogVisibilityInteractor
+
+ @Before
+ fun setUp() {
+ underTest = kosmos.volumeDialogVisibilityInteractor
+ }
+
+ @Test
+ fun testShowRequest_visible() =
+ with(kosmos) {
+ testScope.runTest {
+ runCurrent()
+ val visibilityModel by collectLastValue(underTest.dialogVisibility)
+ fakeVolumeDialogController.onShowRequested(
+ Events.SHOW_REASON_VOLUME_CHANGED,
+ false,
+ ActivityManager.LOCK_TASK_MODE_LOCKED,
+ )
+ runCurrent()
+
+ assertThat(visibilityModel!!)
+ .isEqualTo(
+ VolumeDialogVisibilityModel.Visible(
+ Events.SHOW_REASON_VOLUME_CHANGED,
+ false,
+ ActivityManager.LOCK_TASK_MODE_LOCKED,
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testDismissRequest_dismissed() =
+ with(kosmos) {
+ testScope.runTest {
+ runCurrent()
+ val visibilityModel by collectLastValue(underTest.dialogVisibility)
+ fakeVolumeDialogController.onShowRequested(
+ Events.SHOW_REASON_VOLUME_CHANGED,
+ false,
+ ActivityManager.LOCK_TASK_MODE_LOCKED,
+ )
+ runCurrent()
+
+ fakeVolumeDialogController.onDismissRequested(Events.DISMISS_REASON_SCREEN_OFF)
+
+ assertThat(visibilityModel!!)
+ .isEqualTo(
+ VolumeDialogVisibilityModel.Dismissed(Events.DISMISS_REASON_SCREEN_OFF)
+ )
+ }
+ }
+
+ @Test
+ fun testTimeout_dismissed() =
+ with(kosmos) {
+ testScope.runTest {
+ runCurrent()
+ underTest.resetDismissTimeout()
+ val visibilityModel by collectLastValue(underTest.dialogVisibility)
+ fakeVolumeDialogController.onShowRequested(
+ Events.SHOW_REASON_VOLUME_CHANGED,
+ false,
+ ActivityManager.LOCK_TASK_MODE_LOCKED,
+ )
+ runCurrent()
+
+ advanceTimeBy(1.days)
+
+ assertThat(visibilityModel!!)
+ .isEqualTo(VolumeDialogVisibilityModel.Dismissed(Events.DISMISS_REASON_TIMEOUT))
+ }
+ }
+
+ @Test
+ fun testResetTimeoutInterruptsEvents() =
+ with(kosmos) {
+ testScope.runTest {
+ runCurrent()
+ underTest.resetDismissTimeout()
+ val visibilityModel by collectLastValue(underTest.dialogVisibility)
+ fakeVolumeDialogController.onShowRequested(
+ Events.SHOW_REASON_VOLUME_CHANGED,
+ false,
+ ActivityManager.LOCK_TASK_MODE_LOCKED,
+ )
+ runCurrent()
+
+ advanceTimeBy(dialogTimeoutDuration / 2)
+ underTest.resetDismissTimeout()
+ advanceTimeBy(dialogTimeoutDuration / 2)
+ underTest.resetDismissTimeout()
+ advanceTimeBy(dialogTimeoutDuration / 2)
+
+ assertThat(visibilityModel)
+ .isInstanceOf(VolumeDialogVisibilityModel.Visible::class.java)
+ }
+ }
+}
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 1c09f84b6fd0..94b0b5f67934 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -540,6 +540,7 @@
<!-- Overridden by values-television/styles.xml with tv-specific settings -->
<style name="volume_dialog_theme" parent="Theme.SystemUI">
<item name="android:windowIsFloating">true</item>
+ <item name="android:showWhenLocked">true</item>
</style>
<style name="Theme.SystemUI.DayNightDialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog"/>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerExt.kt
new file mode 100644
index 000000000000..441cbb33f821
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerExt.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.policy
+
+import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.onStart
+
+/** [DevicePostureController.getDevicePosture] as a [Flow]. */
+@DevicePostureInt
+fun DevicePostureController.devicePosture(): Flow<Int> =
+ conflatedCallbackFlow {
+ val callback = DevicePostureController.Callback { posture -> trySend(posture) }
+ addCallback(callback)
+ awaitClose { removeCallback(callback) }
+ }
+ .onStart { emit(devicePosture) }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt
deleted file mode 100644
index b93714ae4fd4..000000000000
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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.volume.dialog
-
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.plugins.VolumeDialog
-import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
-import com.android.systemui.volume.dialog.dagger.VolumeDialogPluginComponent
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-
-class NewVolumeDialogPlugin
-@Inject
-constructor(
- @Application private val applicationCoroutineScope: CoroutineScope,
- private val volumeDialogPluginComponentFactory: VolumeDialogPluginComponent.Factory,
-) : VolumeDialog {
-
- private var volumeDialogPluginComponent: VolumeDialogPluginComponent? = null
- private var job: Job? = null
-
- override fun init(windowType: Int, callback: VolumeDialog.Callback?) {
- job =
- applicationCoroutineScope.launch {
- coroutineScope {
- volumeDialogPluginComponent = volumeDialogPluginComponentFactory.create(this)
- }
- }
- }
-
- private fun showDialog() {
- val volumeDialogPluginComponent =
- volumeDialogPluginComponent ?: error("Creating dialog before init was called")
- volumeDialogPluginComponent.coroutineScope().launch {
- coroutineScope {
- val volumeDialogComponent: VolumeDialogComponent =
- volumeDialogPluginComponent.volumeDialogComponentFactory().create(this)
- with(volumeDialogComponent.volumeDialog()) {
- setOnDismissListener { volumeDialogComponent.coroutineScope().cancel() }
- show()
- }
- }
- }
- }
-
- override fun destroy() {
- job?.cancel()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
index 74e823e9f592..7476c6a279f3 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
@@ -20,15 +20,39 @@ import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.ContextThemeWrapper
+import android.view.MotionEvent
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.res.R
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.ui.binder.VolumeDialogBinder
import javax.inject.Inject
-class VolumeDialog @Inject constructor(@Application context: Context) :
- Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) {
+class VolumeDialog
+@Inject
+constructor(
+ @Application context: Context,
+ private val dialogBinder: VolumeDialogBinder,
+ private val visibilityInteractor: VolumeDialogVisibilityInteractor,
+) : Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.volume_dialog)
+ dialogBinder.bind(this)
+ }
+
+ /**
+ * NOTE: This will be called with ACTION_OUTSIDE MotionEvents for touches that occur outside of
+ * the touchable region of the volume dialog (as returned by [.onComputeInternalInsets]) even if
+ * those touches occurred within the bounds of the volume dialog.
+ */
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ if (isShowing) {
+ if (event.action == MotionEvent.ACTION_OUTSIDE) {
+ visibilityInteractor.dismissDialog(Events.DISMISS_REASON_TOUCH_OUTSIDE)
+ return true
+ }
+ }
+ return false
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
index a2e81d942ae2..4b7a9782cc6b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
@@ -18,12 +18,10 @@ package com.android.systemui.volume.dialog
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.plugins.VolumeDialog
-import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
import com.android.systemui.volume.dialog.dagger.VolumeDialogPluginComponent
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@@ -34,31 +32,17 @@ constructor(
private val volumeDialogPluginComponentFactory: VolumeDialogPluginComponent.Factory,
) : VolumeDialog {
- private var volumeDialogPluginComponent: VolumeDialogPluginComponent? = null
private var job: Job? = null
override fun init(windowType: Int, callback: VolumeDialog.Callback?) {
job =
applicationCoroutineScope.launch {
coroutineScope {
- volumeDialogPluginComponent = volumeDialogPluginComponentFactory.create(this)
- }
- }
- }
+ val component = volumeDialogPluginComponentFactory.create(this)
- private fun showDialog() {
- val volumeDialogPluginComponent =
- volumeDialogPluginComponent ?: error("Creating dialog before init was called")
- volumeDialogPluginComponent.coroutineScope().launch {
- coroutineScope {
- val volumeDialogComponent: VolumeDialogComponent =
- volumeDialogPluginComponent.volumeDialogComponentFactory().create(this)
- with(volumeDialogComponent.volumeDialog()) {
- setOnDismissListener { volumeDialogComponent.coroutineScope().cancel() }
- show()
+ component.viewModel().activate()
}
}
- }
}
override fun destroy() {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt
index 82612a79f6ce..4e0098ccdf99 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt
@@ -19,6 +19,7 @@ package com.android.systemui.volume.dialog.dagger
import com.android.systemui.volume.dialog.dagger.module.VolumeDialogPluginModule
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogPluginViewModel
import dagger.BindsInstance
import dagger.Subcomponent
import kotlinx.coroutines.CoroutineScope
@@ -31,15 +32,7 @@ import kotlinx.coroutines.CoroutineScope
@Subcomponent(modules = [VolumeDialogPluginModule::class])
interface VolumeDialogPluginComponent {
- /**
- * Provides a coroutine scope to use inside [VolumeDialogPluginScope].
- * [com.android.systemui.volume.dialog.VolumeDialogPlugin] manages the lifecycle of this scope.
- * It's cancelled when the dialog is disposed. This helps to free occupied resources when volume
- * dialog is not shown.
- */
- @VolumeDialogPlugin fun coroutineScope(): CoroutineScope
-
- fun volumeDialogComponentFactory(): VolumeDialogComponent.Factory
+ fun viewModel(): VolumeDialogPluginViewModel
@Subcomponent.Factory
interface Factory {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
index ec7c6cee21ff..2e26fd6de410 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
@@ -20,7 +20,8 @@ import android.annotation.SuppressLint
import android.os.Handler
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.plugins.VolumeDialogController
-import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
import com.android.systemui.volume.dialog.domain.model.VolumeDialogStateModel
import javax.inject.Inject
@@ -40,12 +41,12 @@ private const val BUFFER_CAPACITY = 16
*
* @see VolumeDialogController.Callbacks
*/
-@VolumeDialog
+@VolumeDialogPluginScope
class VolumeDialogCallbacksInteractor
@Inject
constructor(
private val volumeDialogController: VolumeDialogController,
- @VolumeDialog private val coroutineScope: CoroutineScope,
+ @VolumeDialogPlugin private val coroutineScope: CoroutineScope,
@Background private val bgHandler: Handler,
) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
index dd511088cb06..4a709a44b42f 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
@@ -17,7 +17,8 @@
package com.android.systemui.volume.dialog.domain.interactor
import com.android.systemui.plugins.VolumeDialogController
-import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
import com.android.systemui.volume.dialog.domain.model.VolumeDialogStateModel
import javax.inject.Inject
@@ -35,13 +36,13 @@ import kotlinx.coroutines.flow.stateIn
*
* @see [VolumeDialogController]
*/
-@VolumeDialog
+@VolumeDialogPluginScope
class VolumeDialogStateInteractor
@Inject
constructor(
volumeDialogCallbacksInteractor: VolumeDialogCallbacksInteractor,
private val volumeDialogController: VolumeDialogController,
- @VolumeDialog private val coroutineScope: CoroutineScope,
+ @VolumeDialogPlugin private val coroutineScope: CoroutineScope,
) {
val volumeDialogState: Flow<VolumeDialogStateModel> =
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt
new file mode 100644
index 000000000000..6c92754b6f60
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.volume.dialog.domain.interactor
+
+import android.annotation.SuppressLint
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogVisibilityModel
+import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.update
+
+private val maxDialogShowTime: Duration = 3.seconds
+
+/**
+ * Handles Volume Dialog visibility state. It might change from several sources:
+ * - [com.android.systemui.plugins.VolumeDialogController] requests visibility change;
+ * - it might be dismissed by the inactivity timeout;
+ * - it can be dismissed by the user;
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@VolumeDialogPluginScope
+class VolumeDialogVisibilityInteractor
+@Inject
+constructor(
+ @VolumeDialogPlugin coroutineScope: CoroutineScope,
+ callbacksInteractor: VolumeDialogCallbacksInteractor,
+) {
+
+ @SuppressLint("SharedFlowCreation")
+ private val mutableDismissDialogEvents = MutableSharedFlow<Unit>()
+ private val mutableDialogVisibility =
+ MutableStateFlow<VolumeDialogVisibilityModel>(VolumeDialogVisibilityModel.Invisible)
+
+ val dialogVisibility: Flow<VolumeDialogVisibilityModel> = mutableDialogVisibility.asStateFlow()
+
+ init {
+ merge(
+ mutableDismissDialogEvents.mapLatest {
+ delay(maxDialogShowTime)
+ VolumeDialogEventModel.DismissRequested(Events.DISMISS_REASON_TIMEOUT)
+ },
+ callbacksInteractor.event,
+ )
+ .onEach { event ->
+ VolumeDialogVisibilityModel.fromEvent(event)?.let { model ->
+ mutableDialogVisibility.value = model
+ if (model is VolumeDialogVisibilityModel.Visible) {
+ resetDismissTimeout()
+ }
+ }
+ }
+ .launchIn(coroutineScope)
+ }
+
+ /**
+ * Dismisses the dialog with a given [reason]. The new state will be emitted in the
+ * [dialogVisibility].
+ */
+ fun dismissDialog(reason: Int) {
+ mutableDialogVisibility.update {
+ if (it is VolumeDialogVisibilityModel.Dismissed) {
+ it
+ } else {
+ VolumeDialogVisibilityModel.Dismissed(reason)
+ }
+ }
+ }
+
+ /** Resets current dialog timeout. */
+ suspend fun resetDismissTimeout() {
+ mutableDismissDialogEvents.emit(Unit)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt
new file mode 100644
index 000000000000..646445d33f51
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.volume.dialog.domain.model
+
+/** Models current Volume Dialog visibility state. */
+sealed interface VolumeDialogVisibilityModel {
+
+ /** Dialog is currently visible. */
+ data class Visible(val reason: Int, val keyguardLocked: Boolean, val lockTaskModeState: Int) :
+ VolumeDialogVisibilityModel
+
+ /** Dialog has never been shown. So it's just invisible. */
+ interface Invisible : VolumeDialogVisibilityModel {
+ companion object : Invisible
+ }
+
+ /** Dialog has been shown and then dismissed. */
+ data class Dismissed(val reason: Int) : Invisible
+
+ companion object {
+
+ /**
+ * Creates [VolumeDialogVisibilityModel] from appropriate events and returns null otherwise.
+ */
+ fun fromEvent(event: VolumeDialogEventModel): VolumeDialogVisibilityModel? {
+ return when (event) {
+ is VolumeDialogEventModel.DismissRequested -> Dismissed(event.reason)
+ is VolumeDialogEventModel.ShowRequested ->
+ Visible(event.reason, event.keyguardLocked, event.lockTaskModeState)
+ else -> null
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt
new file mode 100644
index 000000000000..59c38c019823
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.volume.dialog.shared
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.VolumeLog
+import com.android.systemui.volume.Events
+import javax.inject.Inject
+
+private const val TAG = "SysUI_VolumeDialog"
+
+/** Logs events related to the Volume Panel. */
+class VolumeDialogLogger @Inject constructor(@VolumeLog private val logBuffer: LogBuffer) {
+
+ fun onShow(reason: Int) {
+ logBuffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { int1 = reason },
+ { "Show: ${Events.SHOW_REASONS[int1]}" },
+ )
+ }
+
+ fun onDismiss(reason: Int) {
+ logBuffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { int1 = reason },
+ { "Dismiss: ${Events.DISMISS_REASONS[int1]}" },
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
new file mode 100644
index 000000000000..3f2c39bba6e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.volume.dialog.ui.binder
+
+import android.app.Dialog
+import android.graphics.Color
+import android.graphics.PixelFormat
+import android.graphics.drawable.ColorDrawable
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.view.WindowManager
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogGravityViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+@VolumeDialogScope
+class VolumeDialogBinder
+@Inject
+constructor(
+ @VolumeDialog private val coroutineScope: CoroutineScope,
+ private val volumeDialogViewBinder: VolumeDialogViewBinder,
+ private val gravityViewModel: VolumeDialogGravityViewModel,
+) {
+
+ fun bind(dialog: Dialog) {
+ with(dialog) {
+ setupWindow(window!!)
+ dialog.setContentView(R.layout.volume_dialog)
+
+ val volumeDialogView: View = dialog.requireViewById(R.id.volume_dialog_container)
+ volumeDialogView.repeatWhenAttached {
+ lifecycleScope.launch { volumeDialogViewBinder.bind(volumeDialogView) }
+ }
+ }
+ }
+
+ /** Configures [Window] for the [Dialog]. */
+ private fun setupWindow(window: Window) =
+ with(window) {
+ clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+ addFlags(
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+ WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
+ WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
+ WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
+ )
+ addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
+
+ requestFeature(Window.FEATURE_NO_TITLE)
+ setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY)
+ setWindowAnimations(-1)
+ setFormat(PixelFormat.TRANSLUCENT)
+
+ attributes =
+ attributes.apply {
+ title = "VolumeDialog" // Not the same as Window#setTitle
+ }
+ setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+
+ gravityViewModel.dialogGravity.onEach { window.setGravity(it) }.launchIn(coroutineScope)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt
new file mode 100644
index 000000000000..df6523c9d750
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.volume.dialog.ui.viewmodel
+
+import android.content.Context
+import android.content.res.Configuration
+import android.view.Gravity
+import androidx.annotation.GravityInt
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.UiBackground
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.DevicePostureController
+import com.android.systemui.statusbar.policy.devicePosture
+import com.android.systemui.statusbar.policy.onConfigChanged
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+@VolumeDialogScope
+class VolumeDialogGravityViewModel
+@Inject
+constructor(
+ @Application private val context: Context,
+ @VolumeDialog private val coroutineScope: CoroutineScope,
+ @UiBackground private val uiBackgroundCoroutineContext: CoroutineContext,
+ configurationController: ConfigurationController,
+ private val devicePostureController: DevicePostureController,
+) {
+
+ @GravityInt private var originalGravity: Int = context.getAbsoluteGravity()
+
+ val dialogGravity: Flow<Int> =
+ combine(
+ devicePostureController.devicePosture(),
+ configurationController.onConfigChanged.onEach { onConfigurationChanged() },
+ ) { devicePosture, configuration ->
+ context.calculateGravity(devicePosture, configuration)
+ }
+ .stateIn(
+ scope = coroutineScope,
+ started = SharingStarted.Eagerly,
+ context.calculateGravity(),
+ )
+
+ private suspend fun onConfigurationChanged() {
+ withContext(uiBackgroundCoroutineContext) { originalGravity = context.getAbsoluteGravity() }
+ }
+
+ @GravityInt
+ private fun Context.calculateGravity(
+ devicePosture: Int = devicePostureController.devicePosture,
+ config: Configuration = resources.configuration,
+ ): Int {
+ val isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE
+ val isHalfOpen = devicePosture == DevicePostureController.DEVICE_POSTURE_HALF_OPENED
+ val gravity =
+ if (isLandscape && isHalfOpen) {
+ originalGravity or Gravity.TOP
+ } else {
+ originalGravity
+ }
+ return getAbsoluteGravity(gravity)
+ }
+}
+
+@GravityInt
+private fun Context.getAbsoluteGravity(
+ gravity: Int = resources.getInteger(R.integer.volume_dialog_gravity)
+): Int = with(resources) { Gravity.getAbsoluteGravity(gravity, configuration.layoutDirection) }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt
new file mode 100644
index 000000000000..329a947f24ef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.volume.dialog.ui.viewmodel
+
+import android.app.Dialog
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.plugins.VolumeDialogController
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogVisibilityModel
+import com.android.systemui.volume.dialog.shared.VolumeDialogLogger
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@VolumeDialogPluginScope
+class VolumeDialogPluginViewModel
+@Inject
+constructor(
+ private val componentFactory: VolumeDialogComponent.Factory,
+ private val dialogVisibilityInteractor: VolumeDialogVisibilityInteractor,
+ private val controller: VolumeDialogController,
+ private val logger: VolumeDialogLogger,
+) : ExclusiveActivatable() {
+
+ override suspend fun onActivated(): Nothing {
+ coroutineScope {
+ dialogVisibilityInteractor.dialogVisibility
+ .mapLatest { visibilityModel ->
+ with(visibilityModel) {
+ if (this is VolumeDialogVisibilityModel.Visible) {
+ showDialog(reason, keyguardLocked, lockTaskModeState)
+ }
+ if (this is VolumeDialogVisibilityModel.Dismissed) {
+ Events.writeEvent(Events.EVENT_DISMISS_DIALOG, reason)
+ logger.onDismiss(reason)
+ }
+ }
+ }
+ .launchIn(this)
+ }
+ awaitCancellation()
+ }
+
+ suspend fun showDialog(reason: Int, keyguardLocked: Boolean, lockTaskModeState: Int): Unit =
+ coroutineScope {
+ logger.onShow(reason)
+
+ controller.notifyVisible(true)
+
+ val volumeDialogComponent: VolumeDialogComponent = componentFactory.create(this)
+ val dialog =
+ volumeDialogComponent.volumeDialog().apply {
+ setOnDismissListener {
+ volumeDialogComponent.coroutineScope().cancel()
+ dialogVisibilityInteractor.dismissDialog(Events.DISMISS_REASON_UNKNOWN)
+ }
+ }
+ launch { dialog.awaitShow() }
+
+ Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, keyguardLocked)
+ }
+}
+
+/** Shows [Dialog] until suspend function is cancelled. */
+private suspend fun Dialog.awaitShow() =
+ suspendCancellableCoroutine<Unit> {
+ show()
+ it.invokeOnCancellation { dismiss() }
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
index f9e91aee4922..30c8c15387eb 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
@@ -19,11 +19,12 @@ package com.android.systemui.volume.dialog.ui.viewmodel
import com.android.systemui.lifecycle.ExclusiveActivatable
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
class VolumeDialogViewModel @AssistedInject constructor() : ExclusiveActivatable() {
override suspend fun onActivated(): Nothing {
- TODO("Not yet implemented")
+ awaitCancellation()
}
@AssistedFactory
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt
new file mode 100644
index 000000000000..e4a2a874b848
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt
@@ -0,0 +1,195 @@
+/*
+ * 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.plugins
+
+import android.media.AudioManager
+import android.media.AudioManager.CsdWarning
+import android.os.Handler
+import android.os.VibrationEffect
+import androidx.core.util.getOrElse
+import java.util.concurrent.CopyOnWriteArraySet
+
+class FakeVolumeDialogController(private val audioManager: AudioManager) : VolumeDialogController {
+
+ var isVisible: Boolean = false
+ private set
+
+ var hasScheduledTouchFeedback: Boolean = false
+ private set
+
+ var vibrationEffect: VibrationEffect? = null
+ private set
+
+ var hasUserActivity: Boolean = false
+ private set
+
+ private var hasVibrator: Boolean = true
+
+ private val state = VolumeDialogController.State()
+ private val callbacks = CopyOnWriteArraySet<VolumeDialogController.Callbacks>()
+
+ override fun setActiveStream(stream: Int) {
+ // ensure streamState existence for the active stream
+ state.states.getOrElse(stream) {
+ VolumeDialogController.StreamState().also { streamState ->
+ state.states.put(stream, streamState)
+ }
+ }
+ state.activeStream = stream
+ }
+
+ override fun setStreamVolume(stream: Int, userLevel: Int) {
+ val streamState =
+ state.states.getOrElse(stream) {
+ VolumeDialogController.StreamState().also { streamState ->
+ state.states.put(stream, streamState)
+ }
+ }
+ streamState.level = userLevel.coerceIn(streamState.levelMin, streamState.levelMax)
+ }
+
+ override fun setRingerMode(ringerModeNormal: Int, external: Boolean) {
+ if (external) {
+ state.ringerModeExternal = ringerModeNormal
+ } else {
+ state.ringerModeInternal = ringerModeNormal
+ }
+ }
+
+ fun setHasVibrator(hasVibrator: Boolean) {
+ this.hasVibrator = hasVibrator
+ }
+
+ override fun hasVibrator(): Boolean = hasVibrator
+
+ override fun vibrate(effect: VibrationEffect) {
+ vibrationEffect = effect
+ }
+
+ override fun scheduleTouchFeedback() {
+ hasScheduledTouchFeedback = true
+ }
+
+ fun resetScheduledTouchFeedback() {
+ hasScheduledTouchFeedback = false
+ }
+
+ override fun getAudioManager(): AudioManager = audioManager
+
+ override fun notifyVisible(visible: Boolean) {
+ isVisible = visible
+ }
+
+ override fun addCallback(callbacks: VolumeDialogController.Callbacks?, handler: Handler?) {
+ this.callbacks.add(callbacks)
+ }
+
+ override fun removeCallback(callbacks: VolumeDialogController.Callbacks?) {
+ this.callbacks.remove(callbacks)
+ }
+
+ override fun userActivity() {
+ hasUserActivity = true
+ }
+
+ fun resetUserActivity() {
+ hasUserActivity = false
+ }
+
+ override fun getState() {
+ callbacks.sendEvent { it.onStateChanged(state) }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onShowRequested */
+ fun onShowRequested(reason: Int, keyguardLocked: Boolean, lockTaskModeState: Int) {
+ callbacks.sendEvent { it.onShowRequested(reason, keyguardLocked, lockTaskModeState) }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onDismissRequested */
+ fun onDismissRequested(reason: Int) {
+ callbacks.sendEvent { it.onDismissRequested(reason) }
+ }
+
+ /**
+ * @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onLayoutDirectionChanged
+ */
+ fun onLayoutDirectionChanged(layoutDirection: Int) {
+ callbacks.sendEvent { it.onLayoutDirectionChanged(layoutDirection) }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onConfigurationChanged */
+ fun onConfigurationChanged() {
+ callbacks.sendEvent { it.onConfigurationChanged() }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onShowVibrateHint */
+ fun onShowVibrateHint() {
+ callbacks.sendEvent { it.onShowVibrateHint() }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onShowSilentHint */
+ fun onShowSilentHint() {
+ callbacks.sendEvent { it.onShowSilentHint() }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onScreenOff */
+ fun onScreenOff() {
+ callbacks.sendEvent { it.onScreenOff() }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onShowSafetyWarning */
+ fun onShowSafetyWarning(flags: Int) {
+ callbacks.sendEvent { it.onShowSafetyWarning(flags) }
+ }
+
+ /**
+ * @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onAccessibilityModeChanged
+ */
+ fun onAccessibilityModeChanged(showA11yStream: Boolean?) {
+ callbacks.sendEvent { it.onAccessibilityModeChanged(showA11yStream) }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onShowCsdWarning */
+ fun onShowCsdWarning(@CsdWarning csdWarning: Int, durationMs: Int) {
+ callbacks.sendEvent { it.onShowCsdWarning(csdWarning, durationMs) }
+ }
+
+ /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onVolumeChangedFromKey */
+ fun onVolumeChangedFromKey() {
+ callbacks.sendEvent { it.onVolumeChangedFromKey() }
+ }
+
+ override fun getCaptionsEnabledState(checkForSwitchState: Boolean) {
+ error("Unsupported for the new Volume Dialog")
+ }
+
+ override fun setCaptionsEnabledState(enabled: Boolean) {
+ error("Unsupported for the new Volume Dialog")
+ }
+
+ override fun getCaptionsComponentState(fromTooltip: Boolean) {
+ error("Unsupported for the new Volume Dialog")
+ }
+}
+
+private inline fun CopyOnWriteArraySet<VolumeDialogController.Callbacks>.sendEvent(
+ event: (callback: VolumeDialogController.Callbacks) -> Unit
+) {
+ for (callback in this) {
+ event(callback)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/VolumeDialogControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/VolumeDialogControllerKosmos.kt
new file mode 100644
index 000000000000..2f6d4fa32e65
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/VolumeDialogControllerKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.plugins
+
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.kotlin.mock
+
+val Kosmos.fakeVolumeDialogController by Kosmos.Fixture { FakeVolumeDialogController(mock {}) }
+var Kosmos.volumeDialogController: VolumeDialogController by
+ Kosmos.Fixture { fakeVolumeDialogController }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt
new file mode 100644
index 000000000000..db9c48d9be6f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.volume.dialog.domain.interactor
+
+import android.os.Handler
+import android.os.looper
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.plugins.volumeDialogController
+
+val Kosmos.volumeDialogCallbacksInteractor: VolumeDialogCallbacksInteractor by
+ Kosmos.Fixture {
+ VolumeDialogCallbacksInteractor(
+ volumeDialogController = volumeDialogController,
+ coroutineScope = applicationCoroutineScope,
+ bgHandler = Handler(looper),
+ )
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
index 869b3c65d70e..e73539eac6f1 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
@@ -14,21 +14,12 @@
* limitations under the License.
*/
-package com.android.systemui.volume.dialog
+package com.android.systemui.volume.dialog.domain.interactor
-import android.app.Dialog
-import android.content.Context
-import android.os.Bundle
-import android.view.ContextThemeWrapper
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.res.R
-import javax.inject.Inject
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
-class NewVolumeDialog @Inject constructor(@Application context: Context) :
- Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.volume_dialog)
+val Kosmos.volumeDialogVisibilityInteractor by
+ Kosmos.Fixture {
+ VolumeDialogVisibilityInteractor(applicationCoroutineScope, volumeDialogCallbacksInteractor)
}
-}