diff options
19 files changed, 538 insertions, 56 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 index 7ce421a5aa62..06a3e8b0a766 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.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.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 3fdf86a923fb..cd8cdc8573bd 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 000000000000..2aeaa5cd7248 --- /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 f7d6d90ef6f0..2668589be1b3 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 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.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.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.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 @@ class VolumeDialogVisibilityInteractor 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 @@ constructor( }, 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 @@ constructor( * [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 @@ constructor( 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 db196347d4a9..2dd0bdab93d1 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.Events 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 index 646445d33f51..56a707d86598 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 @@ sealed interface VolumeDialogVisibilityModel { /** 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 9c88303106ae..9452d8c0dcd4 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.CoroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +/** Binds the Volume Dialog itself. */ @VolumeDialogScope class VolumeDialogBinder @Inject @@ -47,9 +48,13 @@ constructor( 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 600d17603964..23e6eac05ea8 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 000000000000..9fcd77716fb6 --- /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 000000000000..4eae3b9a8da5 --- /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 df6523c9d750..112afb1debf5 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.onEach 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 8aa0d09157ad..f336d469abf6 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.cancel 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 @@ constructor( 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 @@ constructor( 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 000000000000..da9be98dd332 --- /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 30c8c15387eb..84c837c9033d 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 000000000000..db35ca7c12ad --- /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 000000000000..291dfc0430e2 --- /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 e73539eac6f1..7376c7fb1495 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 @@ package com.android.systemui.volume.dialog.domain.interactor 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 000000000000..a5074ebe0119 --- /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 000000000000..138256330b2b --- /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 } |