1 /*
<lambda>null2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.systemui.volume.dialog.settings.ui.viewmodel
18
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.annotation.SuppressLint
22 import android.content.Context
23 import android.graphics.ColorFilter
24 import android.graphics.drawable.Drawable
25 import android.media.session.PlaybackState
26 import androidx.annotation.ColorInt
27 import com.airbnb.lottie.LottieComposition
28 import com.airbnb.lottie.LottieCompositionFactory
29 import com.airbnb.lottie.LottieDrawable
30 import com.airbnb.lottie.LottieProperty
31 import com.airbnb.lottie.SimpleColorFilter
32 import com.airbnb.lottie.model.KeyPath
33 import com.airbnb.lottie.value.LottieValueCallback
34 import com.android.internal.R as internalR
35 import com.android.internal.logging.UiEventLogger
36 import com.android.systemui.dagger.qualifiers.Application
37 import com.android.systemui.dagger.qualifiers.UiBackground
38 import com.android.systemui.lottie.await
39 import com.android.systemui.res.R
40 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
41 import com.android.systemui.volume.dialog.settings.domain.VolumeDialogSettingsButtonInteractor
42 import com.android.systemui.volume.dialog.ui.VolumeDialogUiEvent
43 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
44 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
45 import com.android.systemui.volume.panel.shared.model.filterData
46 import javax.inject.Inject
47 import kotlin.coroutines.CoroutineContext
48 import kotlin.coroutines.resume
49 import kotlinx.coroutines.CoroutineScope
50 import kotlinx.coroutines.channels.BufferOverflow
51 import kotlinx.coroutines.flow.Flow
52 import kotlinx.coroutines.flow.FlowCollector
53 import kotlinx.coroutines.flow.SharingStarted
54 import kotlinx.coroutines.flow.buffer
55 import kotlinx.coroutines.flow.distinctUntilChangedBy
56 import kotlinx.coroutines.flow.filterNotNull
57 import kotlinx.coroutines.flow.first
58 import kotlinx.coroutines.flow.flatMapLatest
59 import kotlinx.coroutines.flow.flow
60 import kotlinx.coroutines.flow.flowOf
61 import kotlinx.coroutines.flow.flowOn
62 import kotlinx.coroutines.flow.runningFold
63 import kotlinx.coroutines.flow.stateIn
64 import kotlinx.coroutines.flow.transform
65 import kotlinx.coroutines.suspendCancellableCoroutine
66
67 class VolumeDialogSettingsButtonViewModel
68 @Inject
69 constructor(
70 @Application private val context: Context,
71 @UiBackground private val uiBgCoroutineContext: CoroutineContext,
72 @VolumeDialog private val coroutineScope: CoroutineScope,
73 mediaOutputInteractor: MediaOutputInteractor,
74 private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
75 private val interactor: VolumeDialogSettingsButtonInteractor,
76 private val uiEventLogger: UiEventLogger,
77 ) {
78
79 @SuppressLint("UseCompatLoadingForDrawables")
80 private val drawables: Flow<Drawables> =
81 flow {
82 val color = context.getColor(internalR.color.materialColorPrimary)
83 emit(
84 Drawables(
85 start =
86 LottieCompositionFactory.fromRawRes(context, R.raw.audio_bars_in)
87 .await()
88 .toDrawable { setColor(color) },
89 playing =
90 LottieCompositionFactory.fromRawRes(context, R.raw.audio_bars_playing)
91 .await()
92 .toDrawable {
93 repeatCount = LottieDrawable.INFINITE
94 repeatMode = LottieDrawable.RESTART
95 setColor(color)
96 },
97 stop =
98 LottieCompositionFactory.fromRawRes(context, R.raw.audio_bars_out)
99 .await()
100 .toDrawable { setColor(color) },
101 idle = context.getDrawable(R.drawable.audio_bars_idle)!!,
102 )
103 )
104 }
105 .buffer()
106 .flowOn(uiBgCoroutineContext)
107 .stateIn(coroutineScope, SharingStarted.Eagerly, null)
108 .filterNotNull()
109
110 val isVisible = interactor.isVisible
111 val icon: Flow<Drawable> =
112 mediaOutputInteractor.defaultActiveMediaSession
113 .filterData()
114 .flatMapLatest { session ->
115 if (session == null) {
116 flowOf(null)
117 } else {
118 mediaDeviceSessionInteractor.playbackState(session)
119 }
120 }
121 .runningFold(null) { playbackStates: PlaybackStates?, playbackState: PlaybackState? ->
122 val isCurrentActive = playbackState?.isActive ?: false
123 if (playbackStates != null && isCurrentActive == playbackState?.isActive) {
124 return@runningFold playbackStates
125 }
126 playbackStates?.copy(
127 isPreviousActive = playbackStates.isCurrentActive,
128 isCurrentActive = isCurrentActive,
129 ) ?: PlaybackStates(isPreviousActive = null, isCurrentActive = isCurrentActive)
130 }
131 .filterNotNull()
132 // only apply the most recent state if we wait for the animation.
133 .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
134 // distinct again because the changed state might've been dropped by the buffer
135 .distinctUntilChangedBy { it.isCurrentActive }
136 .transform { emitDrawables(it) }
137 .runningFold(null) { previous: Drawable?, current: Drawable ->
138 // wait for the previous animation to finish before starting the new one
139 // this also waits for the current loop of the playing animation to finish
140 (previous as? LottieDrawable)?.awaitFinish()
141 (current as? LottieDrawable)?.start()
142 current
143 }
144 .filterNotNull()
145
146 private suspend fun FlowCollector<Drawable>.emitDrawables(playbackStates: PlaybackStates) {
147 val animations = drawables.first()
148 val stateChanged =
149 playbackStates.isPreviousActive != null &&
150 playbackStates.isPreviousActive != playbackStates.isCurrentActive
151 if (playbackStates.isCurrentActive) {
152 if (stateChanged) {
153 emit(animations.start)
154 }
155 emit(animations.playing)
156 } else {
157 if (stateChanged) {
158 emit(animations.stop)
159 }
160 emit(animations.idle)
161 }
162 }
163
164 fun onButtonClicked() {
165 interactor.onButtonClicked()
166 uiEventLogger.log(VolumeDialogUiEvent.VOLUME_DIALOG_SETTINGS_CLICK)
167 }
168
169 private data class PlaybackStates(val isPreviousActive: Boolean?, val isCurrentActive: Boolean)
170
171 private data class Drawables(
172 val start: LottieDrawable,
173 val playing: LottieDrawable,
174 val stop: LottieDrawable,
175 val idle: Drawable,
176 )
177 }
178
<lambda>null179 private fun LottieComposition.toDrawable(setup: LottieDrawable.() -> Unit = {}): LottieDrawable =
drawablenull180 LottieDrawable().also { drawable ->
181 drawable.composition = this
182 drawable.setup()
183 }
184
185 /** Suspends until current loop of the repeating animation is finished */
continuationnull186 private suspend fun LottieDrawable.awaitFinish() = suspendCancellableCoroutine { continuation ->
187 if (!isRunning) {
188 continuation.resume(Unit)
189 return@suspendCancellableCoroutine
190 }
191 val listener =
192 object : AnimatorListenerAdapter() {
193 override fun onAnimationRepeat(animation: Animator) {
194 continuation.resume(Unit)
195 removeAnimatorListener(this)
196 }
197
198 override fun onAnimationEnd(animation: Animator) {
199 continuation.resume(Unit)
200 removeAnimatorListener(this)
201 }
202
203 override fun onAnimationCancel(animation: Animator) {
204 continuation.resume(Unit)
205 removeAnimatorListener(this)
206 }
207 }
208 addAnimatorListener(listener)
209 continuation.invokeOnCancellation { removeAnimatorListener(listener) }
210 }
211
212 /**
213 * Overrides colors of the [LottieDrawable] to a specified [color]
214 *
215 * @see com.airbnb.lottie.LottieAnimationView
216 */
LottieDrawablenull217 private fun LottieDrawable.setColor(@ColorInt color: Int) {
218 val callback = LottieValueCallback<ColorFilter>(SimpleColorFilter(color))
219 addValueCallback(KeyPath("**"), LottieProperty.COLOR_FILTER, callback)
220 }
221