• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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