Add show-hide animations for the new Volume Dialog
Flag: com.android.systemui.volume_redesign
Test: passes presubmits
Bug: 369994090
Change-Id: Icba6f4fba4c0ebf3135e187d3ab3fd454ebaa726
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
index 7ce421a..06a3e8b 100644
--- 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
@@ -27,7 +27,7 @@
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.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
import com.google.common.truth.Truth.assertThat
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt
index 3fdf86a..cd8cdc8 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt
@@ -17,6 +17,13 @@
package com.android.systemui.volume.dialog.dagger.module
import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
+import com.android.systemui.volume.dialog.utils.VolumeTracer
+import com.android.systemui.volume.dialog.utils.VolumeTracerImpl
+import dagger.Binds
import dagger.Module
-@Module(subcomponents = [VolumeDialogComponent::class]) interface VolumeDialogPluginModule
+@Module(subcomponents = [VolumeDialogComponent::class])
+interface VolumeDialogPluginModule {
+
+ @Binds fun bindVolumeTracer(volumeTracer: VolumeTracerImpl): VolumeTracer
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/data/VolumeDialogVisibilityRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/data/VolumeDialogVisibilityRepository.kt
new file mode 100644
index 0000000..2aeaa5c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/data/VolumeDialogVisibilityRepository.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.data
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+
+@SysUISingleton
+class VolumeDialogVisibilityRepository @Inject constructor() {
+
+ private val mutableDialogVisibility =
+ MutableStateFlow<VolumeDialogVisibilityModel>(VolumeDialogVisibilityModel.Invisible)
+ val dialogVisibility: Flow<VolumeDialogVisibilityModel> = mutableDialogVisibility.asStateFlow()
+
+ fun updateVisibility(
+ update: (current: VolumeDialogVisibilityModel) -> VolumeDialogVisibilityModel
+ ) {
+ mutableDialogVisibility.update(update)
+ }
+}
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
index f7d6d90..2668589b 100644
--- 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
@@ -20,8 +20,12 @@
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.data.VolumeDialogVisibilityRepository
import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
-import com.android.systemui.volume.dialog.domain.model.VolumeDialogVisibilityModel
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel.Dismissed
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel.Visible
+import com.android.systemui.volume.dialog.utils.VolumeTracer
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@@ -30,13 +34,11 @@
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.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.update
private val MAX_DIALOG_SHOW_TIME: Duration = 3.seconds
@@ -53,14 +55,13 @@
constructor(
@VolumeDialogPlugin coroutineScope: CoroutineScope,
callbacksInteractor: VolumeDialogCallbacksInteractor,
+ private val tracer: VolumeTracer,
+ private val repository: VolumeDialogVisibilityRepository,
) {
@SuppressLint("SharedFlowCreation")
private val mutableDismissDialogEvents = MutableSharedFlow<Unit>()
- private val mutableDialogVisibility =
- MutableStateFlow<VolumeDialogVisibilityModel>(VolumeDialogVisibilityModel.Invisible)
-
- val dialogVisibility: Flow<VolumeDialogVisibilityModel> = mutableDialogVisibility.asStateFlow()
+ val dialogVisibility: Flow<VolumeDialogVisibilityModel> = repository.dialogVisibility
init {
merge(
@@ -70,12 +71,11 @@
},
callbacksInteractor.event,
)
- .onEach { event ->
- VolumeDialogVisibilityModel.fromEvent(event)?.let { model ->
- mutableDialogVisibility.value = model
- if (model is VolumeDialogVisibilityModel.Visible) {
- resetDismissTimeout()
- }
+ .mapNotNull { it.toVisibilityModel() }
+ .onEach { model ->
+ updateVisibility { model }
+ if (model is VolumeDialogVisibilityModel.Visible) {
+ resetDismissTimeout()
}
}
.launchIn(coroutineScope)
@@ -86,9 +86,9 @@
* [dialogVisibility].
*/
fun dismissDialog(reason: Int) {
- mutableDialogVisibility.update {
- if (it is VolumeDialogVisibilityModel.Dismissed) {
- it
+ updateVisibility { visibilityModel ->
+ if (visibilityModel is VolumeDialogVisibilityModel.Dismissed) {
+ visibilityModel
} else {
VolumeDialogVisibilityModel.Dismissed(reason)
}
@@ -99,4 +99,19 @@
suspend fun resetDismissTimeout() {
mutableDismissDialogEvents.emit(Unit)
}
+
+ private fun updateVisibility(
+ update: (VolumeDialogVisibilityModel) -> VolumeDialogVisibilityModel
+ ) {
+ repository.updateVisibility { update(it).also(tracer::traceVisibilityStart) }
+ }
+
+ private fun VolumeDialogEventModel.toVisibilityModel(): VolumeDialogVisibilityModel? {
+ return when (this) {
+ is VolumeDialogEventModel.DismissRequested -> Dismissed(reason)
+ is VolumeDialogEventModel.ShowRequested ->
+ Visible(reason, keyguardLocked, lockTaskModeState)
+ else -> null
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractor.kt
index db19634..2dd0bda 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractor.kt
@@ -23,7 +23,7 @@
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
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.model.VolumeDialogVisibilityModel
import com.android.systemui.volume.panel.domain.interactor.VolumePanelGlobalStateInteractor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogVisibilityModel.kt
similarity index 65%
rename from packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt
rename to packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogVisibilityModel.kt
index 646445d..56a707d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogVisibilityModel.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.volume.dialog.domain.model
+package com.android.systemui.volume.dialog.shared.model
/** Models current Volume Dialog visibility state. */
sealed interface VolumeDialogVisibilityModel {
@@ -30,19 +30,4 @@
/** 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/ui/binder/VolumeDialogBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
index 9c88303..9452d8c 100644
--- 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
@@ -33,6 +33,7 @@
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+/** Binds the Volume Dialog itself. */
@VolumeDialogScope
class VolumeDialogBinder
@Inject
@@ -47,9 +48,13 @@
with(dialog) {
setupWindow(window!!)
dialog.setContentView(R.layout.volume_dialog)
+ dialog.setCanceledOnTouchOutside(true)
settingsButtonViewBinder.bind(dialog.requireViewById(R.id.settings_container))
- volumeDialogViewBinder.bind(dialog.requireViewById(R.id.volume_dialog_container))
+ volumeDialogViewBinder.bind(
+ dialog,
+ dialog.requireViewById(R.id.volume_dialog_container),
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
index 600d176..23e6eac 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
@@ -16,32 +16,144 @@
package com.android.systemui.volume.dialog.ui.binder
+import android.app.Dialog
+import android.view.Gravity
import android.view.View
+import com.android.internal.view.RotationPolicy
import com.android.systemui.lifecycle.WindowLifecycleState
import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.lifecycle.setSnapshotBinding
import com.android.systemui.lifecycle.viewModel
+import com.android.systemui.volume.SystemUIInterpolators
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+import com.android.systemui.volume.dialog.ui.utils.JankListenerFactory
+import com.android.systemui.volume.dialog.ui.utils.suspendAnimate
+import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogGravityViewModel
+import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogResourcesViewModel
import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogViewModel
+import com.android.systemui.volume.dialog.utils.VolumeTracer
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapLatest
+/** Binds the root view of the Volume Dialog. */
+@OptIn(ExperimentalCoroutinesApi::class)
@VolumeDialogScope
class VolumeDialogViewBinder
@Inject
-constructor(private val volumeDialogViewModelFactory: VolumeDialogViewModel.Factory) {
+constructor(
+ private val volumeResources: VolumeDialogResourcesViewModel,
+ private val gravityViewModel: VolumeDialogGravityViewModel,
+ private val viewModelFactory: VolumeDialogViewModel.Factory,
+ private val jankListenerFactory: JankListenerFactory,
+ private val tracer: VolumeTracer,
+) {
- fun bind(view: View) {
+ fun bind(dialog: Dialog, view: View) {
+ view.alpha = 0f
view.repeatWhenAttached {
view.viewModel(
traceName = "VolumeDialogViewBinder",
minWindowLifecycleState = WindowLifecycleState.ATTACHED,
- factory = { volumeDialogViewModelFactory.create() },
+ factory = { viewModelFactory.create() },
) { viewModel ->
- view.setSnapshotBinding {}
+ animateVisibility(view, dialog, viewModel.dialogVisibilityModel)
awaitCancellation()
}
}
}
+
+ private fun CoroutineScope.animateVisibility(
+ view: View,
+ dialog: Dialog,
+ visibilityModel: Flow<VolumeDialogVisibilityModel>,
+ ) {
+ visibilityModel
+ .mapLatest {
+ when (it) {
+ is VolumeDialogVisibilityModel.Visible -> {
+ tracer.traceVisibilityEnd(it)
+ calculateTranslationX(view)?.let(view::setTranslationX)
+ view.animateShow(volumeResources.dialogShowDurationMillis.first())
+ }
+ is VolumeDialogVisibilityModel.Dismissed -> {
+ tracer.traceVisibilityEnd(it)
+ view.animateHide(
+ duration = volumeResources.dialogHideDurationMillis.first(),
+ translationX = calculateTranslationX(view),
+ )
+ dialog.dismiss()
+ }
+ is VolumeDialogVisibilityModel.Invisible -> {
+ // do nothing
+ }
+ }
+ }
+ .launchIn(this)
+ }
+
+ private suspend fun calculateTranslationX(view: View): Float? {
+ return if (view.display.rotation == RotationPolicy.NATURAL_ROTATION) {
+ val dialogGravity = gravityViewModel.dialogGravity.first()
+ val isGravityLeft = (dialogGravity and Gravity.LEFT) == Gravity.LEFT
+ if (isGravityLeft) {
+ -1
+ } else {
+ 1
+ } * view.width / 2.0f
+ } else {
+ null
+ }
+ }
+
+ private suspend fun View.animateShow(duration: Long) {
+ animate()
+ .alpha(1f)
+ .translationX(0f)
+ .setDuration(duration)
+ .setInterpolator(SystemUIInterpolators.LogDecelerateInterpolator())
+ .suspendAnimate(jankListenerFactory.show(this, duration))
+ /* TODO(b/369993851)
+ .withEndAction(Runnable {
+ if (!Prefs.getBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, false)) {
+ if (mRingerIcon != null) {
+ mRingerIcon.postOnAnimationDelayed(
+ getSinglePressFor(mRingerIcon), 1500
+ )
+ }
+ }
+ })
+ */
+ }
+
+ private suspend fun View.animateHide(duration: Long, translationX: Float?) {
+ val animator =
+ animate()
+ .alpha(0f)
+ .setDuration(duration)
+ .setInterpolator(SystemUIInterpolators.LogAccelerateInterpolator())
+ /* TODO(b/369993851)
+ .withEndAction(
+ Runnable {
+ mHandler.postDelayed(
+ Runnable {
+ hideRingerDrawer()
+
+ },
+ 50
+ )
+ }
+ )
+ */
+ if (translationX != null) {
+ animator.translationX(translationX)
+ }
+ animator.suspendAnimate(jankListenerFactory.dismiss(this, duration))
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt
new file mode 100644
index 0000000..9fcd777
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.utils
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.view.View
+import com.android.internal.jank.Cuj
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import javax.inject.Inject
+
+/** Provides [Animator.AnimatorListener] to measure Volume CUJ Jank */
+@VolumeDialogPluginScope
+class JankListenerFactory
+@Inject
+constructor(private val interactionJankMonitor: InteractionJankMonitor) {
+
+ fun show(view: View, timeout: Long) = getJunkListener(view, "show", timeout)
+
+ fun update(view: View, timeout: Long) = getJunkListener(view, "update", timeout)
+
+ fun dismiss(view: View, timeout: Long) = getJunkListener(view, "dismiss", timeout)
+
+ private fun getJunkListener(
+ view: View,
+ type: String,
+ timeout: Long,
+ ): Animator.AnimatorListener {
+ return object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator) {
+ interactionJankMonitor.begin(
+ InteractionJankMonitor.Configuration.Builder.withView(
+ Cuj.CUJ_VOLUME_CONTROL,
+ view,
+ )
+ .setTag(type)
+ .setTimeout(timeout)
+ )
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ interactionJankMonitor.end(Cuj.CUJ_VOLUME_CONTROL)
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ interactionJankMonitor.cancel(Cuj.CUJ_VOLUME_CONTROL)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt
new file mode 100644
index 0000000..4eae3b9a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.utils
+
+import android.animation.Animator
+import android.view.ViewPropertyAnimator
+import kotlin.coroutines.resume
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * Starts animation and suspends until it's finished. Cancels the animation if the running coroutine
+ * is cancelled.
+ *
+ * Careful! This method overrides [ViewPropertyAnimator.setListener]. Use [animationListener]
+ * instead.
+ */
+suspend fun ViewPropertyAnimator.suspendAnimate(
+ animationListener: Animator.AnimatorListener? = null
+) = suspendCancellableCoroutine { continuation ->
+ start()
+ setListener(
+ object : Animator.AnimatorListener {
+ override fun onAnimationStart(animation: Animator) {
+ animationListener?.onAnimationStart(animation)
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ continuation.resume(Unit)
+ animationListener?.onAnimationEnd(animation)
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ animationListener?.onAnimationCancel(animation)
+ }
+
+ override fun onAnimationRepeat(animation: Animator) {
+ animationListener?.onAnimationRepeat(animation)
+ }
+ }
+ )
+ continuation.invokeOnCancellation { this.cancel() }
+}
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
index df6523c..112afb1 100644
--- 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
@@ -39,6 +39,7 @@
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
+/** Exposes dialog [GravityInt] for use in the UI layer. */
@VolumeDialogScope
class VolumeDialogGravityViewModel
@Inject
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
index 8aa0d09..f336d46 100644
--- 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
@@ -16,15 +16,14 @@
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 com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.awaitCancellation
@@ -32,8 +31,7 @@
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.flow.onEach
@OptIn(ExperimentalCoroutinesApi::class)
@VolumeDialogPluginScope
@@ -49,6 +47,7 @@
override suspend fun onActivated(): Nothing {
coroutineScope {
dialogVisibilityInteractor.dialogVisibility
+ .onEach { controller.notifyVisible(it is VolumeDialogVisibilityModel.Visible) }
.mapLatest { visibilityModel ->
with(visibilityModel) {
if (this is VolumeDialogVisibilityModel.Visible) {
@@ -78,15 +77,8 @@
dialogVisibilityInteractor.dismissDialog(Events.DISMISS_REASON_UNKNOWN)
}
}
- launch { dialog.awaitShow() }
+ dialog.show()
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/VolumeDialogResourcesViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogResourcesViewModel.kt
new file mode 100644
index 0000000..da9be98
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogResourcesViewModel.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.Resources
+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.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.filterNotNull
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Provides cached resources [Flow]s that update when the configuration changes.
+ *
+ * Consume or use [kotlinx.coroutines.flow.first] to get the value.
+ */
+@VolumeDialogScope
+class VolumeDialogResourcesViewModel
+@Inject
+constructor(
+ @VolumeDialog private val coroutineScope: CoroutineScope,
+ @UiBackground private val uiBackgroundContext: CoroutineContext,
+ private val context: Context,
+ private val configurationController: ConfigurationController,
+) {
+
+ val dialogShowDurationMillis: Flow<Long> = configurationResource {
+ getInteger(R.integer.config_dialogShowAnimationDurationMs).toLong()
+ }
+
+ val dialogHideDurationMillis: Flow<Long> = configurationResource {
+ getInteger(R.integer.config_dialogHideAnimationDurationMs).toLong()
+ }
+
+ private fun <T> configurationResource(get: Resources.() -> T): Flow<T> =
+ configurationController.onConfigChanged
+ .map { context.resources.get() }
+ .onStart { emit(context.resources.get()) }
+ .flowOn(uiBackgroundContext)
+ .stateIn(coroutineScope, SharingStarted.Eagerly, null)
+ .filterNotNull()
+}
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 30c8c15..84c837c 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
@@ -17,11 +17,20 @@
package com.android.systemui.volume.dialog.ui.viewmodel
import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
-class VolumeDialogViewModel @AssistedInject constructor() : ExclusiveActivatable() {
+/** Provides a state for the Volume Dialog. */
+class VolumeDialogViewModel
+@AssistedInject
+constructor(dialogVisibilityInteractor: VolumeDialogVisibilityInteractor) : ExclusiveActivatable() {
+
+ val dialogVisibilityModel: Flow<VolumeDialogVisibilityModel> =
+ dialogVisibilityInteractor.dialogVisibility
override suspend fun onActivated(): Nothing {
awaitCancellation()
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/utils/VolumeTracer.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/utils/VolumeTracer.kt
new file mode 100644
index 0000000..db35ca7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/utils/VolumeTracer.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.utils
+
+import android.os.Trace
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+import javax.inject.Inject
+
+/** Traces the async sections for the Volume Dialog. */
+interface VolumeTracer {
+
+ fun traceVisibilityStart(model: VolumeDialogVisibilityModel)
+
+ fun traceVisibilityEnd(model: VolumeDialogVisibilityModel)
+}
+
+@VolumeDialogPluginScope
+class VolumeTracerImpl @Inject constructor() : VolumeTracer {
+
+ override fun traceVisibilityStart(model: VolumeDialogVisibilityModel) =
+ with(model) { Trace.beginAsyncSection(methodName, tracingCookie) }
+
+ override fun traceVisibilityEnd(model: VolumeDialogVisibilityModel) =
+ with(model) { Trace.endAsyncSection(methodName, tracingCookie) }
+
+ private val VolumeDialogVisibilityModel.tracingCookie
+ get() = this.hashCode()
+
+ private val VolumeDialogVisibilityModel.methodName
+ get() =
+ when (this) {
+ is VolumeDialogVisibilityModel.Visible -> "VolumeDialog#show"
+ is VolumeDialogVisibilityModel.Dismissed -> "VolumeDialog#dismiss"
+ is VolumeDialogVisibilityModel.Invisible -> error("Invisible is unsupported")
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt
new file mode 100644
index 0000000..291dfc0
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.data.VolumeDialogVisibilityRepository
+
+val Kosmos.volumeDialogVisibilityRepository by Kosmos.Fixture { VolumeDialogVisibilityRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
index e73539e..7376c7f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
@@ -18,8 +18,15 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository
+import com.android.systemui.volume.dialog.utils.volumeTracer
val Kosmos.volumeDialogVisibilityInteractor by
Kosmos.Fixture {
- VolumeDialogVisibilityInteractor(applicationCoroutineScope, volumeDialogCallbacksInteractor)
+ VolumeDialogVisibilityInteractor(
+ applicationCoroutineScope,
+ volumeDialogCallbacksInteractor,
+ volumeTracer,
+ volumeDialogVisibilityRepository,
+ )
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/utils/FakeVolumeTracer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/utils/FakeVolumeTracer.kt
new file mode 100644
index 0000000..a5074eb
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/utils/FakeVolumeTracer.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.utils
+
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+
+class FakeVolumeTracer : VolumeTracer {
+
+ override fun traceVisibilityStart(model: VolumeDialogVisibilityModel) {}
+
+ override fun traceVisibilityEnd(model: VolumeDialogVisibilityModel) {}
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/utils/VolumeTracerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/utils/VolumeTracerKosmos.kt
new file mode 100644
index 0000000..1382563
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/utils/VolumeTracerKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.utils
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeVolumeTracer: FakeVolumeTracer by Kosmos.Fixture { FakeVolumeTracer() }
+var Kosmos.volumeTracer: VolumeTracer by Kosmos.Fixture { fakeVolumeTracer }