1 /*
<lambda>null2  * Copyright (C) 2023 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.google.android.wallpaper.weathereffects
18 
19 import android.animation.ValueAnimator
20 import android.app.WallpaperColors
21 import android.content.Context
22 import android.graphics.Bitmap
23 import android.os.Bundle
24 import android.os.SystemClock
25 import android.util.Log
26 import android.util.Size
27 import android.util.SizeF
28 import android.view.SurfaceHolder
29 import androidx.annotation.FloatRange
30 import com.google.android.torus.canvas.engine.CanvasWallpaperEngine
31 import com.google.android.torus.core.wallpaper.listener.LiveWallpaperEventListener
32 import com.google.android.torus.core.wallpaper.listener.LiveWallpaperKeyguardEventListener
33 import com.google.android.wallpaper.weathereffects.domain.WeatherEffectsInteractor
34 import com.google.android.wallpaper.weathereffects.graphics.WeatherEffect
35 import com.google.android.wallpaper.weathereffects.graphics.fog.FogEffect
36 import com.google.android.wallpaper.weathereffects.graphics.fog.FogEffectConfig
37 import com.google.android.wallpaper.weathereffects.graphics.none.NoEffect
38 import com.google.android.wallpaper.weathereffects.graphics.rain.RainEffect
39 import com.google.android.wallpaper.weathereffects.graphics.rain.RainEffectConfig
40 import com.google.android.wallpaper.weathereffects.graphics.snow.SnowEffect
41 import com.google.android.wallpaper.weathereffects.graphics.snow.SnowEffectConfig
42 import com.google.android.wallpaper.weathereffects.graphics.sun.SunEffect
43 import com.google.android.wallpaper.weathereffects.graphics.sun.SunEffectConfig
44 import com.google.android.wallpaper.weathereffects.provider.WallpaperInfoContract
45 import com.google.android.wallpaper.weathereffects.sensor.UserPresenceController
46 import com.google.android.wallpaper.weathereffects.shared.model.WallpaperImageModel
47 import kotlin.math.max
48 import kotlin.math.roundToInt
49 import kotlinx.coroutines.CoroutineScope
50 import kotlinx.coroutines.Job
51 import kotlinx.coroutines.launch
52 
53 class WeatherEngine(
54     defaultHolder: SurfaceHolder,
55     private val applicationScope: CoroutineScope,
56     private val interactor: WeatherEffectsInteractor,
57     private val context: Context,
58     private val isDebugActivity: Boolean = false,
59     hardwareAccelerated: Boolean = true,
60 ) :
61     CanvasWallpaperEngine(defaultHolder, hardwareAccelerated),
62     LiveWallpaperKeyguardEventListener,
63     LiveWallpaperEventListener {
64 
65     private var lockStartTime: Long = 0
66     private var unlockAnimator: ValueAnimator? = null
67 
68     private var backgroundColor: WallpaperColors? = null
69     private var currentAssets: WallpaperImageModel? = null
70     private var activeEffect: WeatherEffect? = null
71         private set(value) {
72             field = value
73             if (shouldTriggerUpdate()) {
74                 startUpdateLoop()
75             } else {
76                 stopUpdateLoop()
77             }
78         }
79 
80     private var collectWallpaperImageJob: Job? = null
81     private var effectTargetIntensity: Float = 1f
82     private var effectIntensity: Float = 0f
83 
84     private var userPresenceController =
85         UserPresenceController(context) { newUserPresence, oldUserPresence ->
86             onUserPresenceChange(newUserPresence, oldUserPresence)
87         }
88 
89     init {
90         /* Load assets. */
91         if (interactor.wallpaperImageModel.value == null) {
92             applicationScope.launch { interactor.loadWallpaper() }
93         }
94     }
95 
96     override fun onCreate(isFirstActiveInstance: Boolean) {
97         Log.d(TAG, "Engine created.")
98         /*
99          * Initialize `effectIntensity` to `effectTargetIntensity` so we show the weather effect
100          * on preview and when `isDebugActivity` is true.
101          *
102          * isPreview() is only reliable after `onCreate`. Thus update the initial value of
103          * `effectIntensity` in case it is not 0.
104          */
105         if (shouldSkipIntensityOutAnimation()) {
106             updateCurrentIntensity(effectTargetIntensity)
107         }
108     }
109 
110     override fun onResize(size: Size) {
111         activeEffect?.resize(size.toSizeF())
112         if (activeEffect is NoEffect) {
113             render { canvas -> activeEffect!!.draw(canvas) }
114         }
115     }
116 
117     override fun onResume() {
118         collectWallpaperImageJob =
119             applicationScope.launch {
120                 interactor.wallpaperImageModel.collect { asset ->
121                     if (asset == null || asset == currentAssets) return@collect
122                     currentAssets = asset
123                     createWeatherEffect(asset.foreground, asset.background, asset.weatherEffect)
124                     updateWallpaperColors(asset.background)
125                 }
126             }
127         if (activeEffect != null) {
128             if (shouldTriggerUpdate()) startUpdateLoop()
129         }
130         userPresenceController.start(context.mainExecutor)
131     }
132 
133     override fun onUpdate(deltaMillis: Long, frameTimeNanos: Long) {
134         activeEffect?.update(deltaMillis, frameTimeNanos)
135 
136         renderWithFpsLimit(frameTimeNanos) { canvas -> activeEffect?.draw(canvas) }
137     }
138 
139     override fun onPause() {
140         stopUpdateLoop()
141         collectWallpaperImageJob?.cancel()
142         activeEffect?.reset()
143         userPresenceController.stop()
144     }
145 
146     override fun onDestroy(isLastActiveInstance: Boolean) {
147         activeEffect?.release()
148         activeEffect = null
149     }
150 
151     override fun onKeyguardGoingAway() {
152         userPresenceController.onKeyguardGoingAway()
153     }
154 
155     override fun onOffsetChanged(xOffset: Float, xOffsetStep: Float) {
156         // No-op.
157     }
158 
159     override fun onZoomChanged(zoomLevel: Float) {
160         // No-op.
161     }
162 
163     override fun onWallpaperReapplied() {
164         // No-op.
165     }
166 
167     override fun shouldZoomOutWallpaper(): Boolean = true
168 
169     override fun computeWallpaperColors(): WallpaperColors? = backgroundColor
170 
171     override fun onWake(extras: Bundle) {
172         userPresenceController.setWakeState(true)
173     }
174 
175     override fun onSleep(extras: Bundle) {
176         userPresenceController.setWakeState(false)
177     }
178 
179     fun setTargetIntensity(@FloatRange(from = 0.0, to = 1.0) intensity: Float) {
180         effectTargetIntensity = intensity
181 
182         /* If we don't want to animate, update the target intensity as it happens. */
183         if (shouldSkipIntensityOutAnimation()) {
184             updateCurrentIntensity(effectTargetIntensity)
185         }
186     }
187 
188     private fun createWeatherEffect(
189         foreground: Bitmap,
190         background: Bitmap,
191         weatherEffect: WallpaperInfoContract.WeatherEffect? = null,
192     ) {
193         activeEffect?.release()
194         activeEffect = null
195 
196         when (weatherEffect) {
197             WallpaperInfoContract.WeatherEffect.RAIN -> {
198                 val rainConfig =
199                     RainEffectConfig(context.assets, context.resources.displayMetrics.density)
200                 activeEffect =
201                     RainEffect(
202                         rainConfig,
203                         foreground,
204                         background,
205                         effectIntensity,
206                         screenSize.toSizeF(),
207                         context.mainExecutor,
208                     )
209             }
210             WallpaperInfoContract.WeatherEffect.FOG -> {
211                 val fogConfig =
212                     FogEffectConfig(context.assets, context.resources.displayMetrics.density)
213 
214                 activeEffect =
215                     FogEffect(
216                         fogConfig,
217                         foreground,
218                         background,
219                         effectIntensity,
220                         screenSize.toSizeF(),
221                     )
222             }
223             WallpaperInfoContract.WeatherEffect.SNOW -> {
224                 val snowConfig =
225                     SnowEffectConfig(context.assets, context.resources.displayMetrics.density)
226                 activeEffect =
227                     SnowEffect(
228                         snowConfig,
229                         foreground,
230                         background,
231                         effectIntensity,
232                         screenSize.toSizeF(),
233                         context.mainExecutor,
234                     )
235             }
236             WallpaperInfoContract.WeatherEffect.SUN -> {
237                 val snowConfig =
238                     SunEffectConfig(context.assets, context.resources.displayMetrics.density)
239                 activeEffect =
240                     SunEffect(
241                         snowConfig,
242                         foreground,
243                         background,
244                         effectIntensity,
245                         screenSize.toSizeF(),
246                     )
247             }
248             else -> {
249                 activeEffect = NoEffect(foreground, background, screenSize.toSizeF())
250             }
251         }
252 
253         updateCurrentIntensity()
254 
255         render { canvas -> activeEffect?.draw(canvas) }
256     }
257 
258     private fun shouldTriggerUpdate(): Boolean {
259         return activeEffect != null && activeEffect !is NoEffect
260     }
261 
262     private fun Size.toSizeF(): SizeF = SizeF(width.toFloat(), height.toFloat())
263 
264     private fun onUserPresenceChange(
265         newUserPresence: UserPresenceController.UserPresence,
266         oldUserPresence: UserPresenceController.UserPresence,
267     ) {
268         playIntensityFadeOutAnimation(getAnimationType(newUserPresence, oldUserPresence))
269     }
270 
271     private fun updateCurrentIntensity(intensity: Float = effectIntensity) {
272         if (effectIntensity != intensity) {
273             effectIntensity = intensity
274         }
275         activeEffect?.setIntensity(effectIntensity)
276     }
277 
278     private fun playIntensityFadeOutAnimation(animationType: AnimationType) {
279         when (animationType) {
280             AnimationType.WAKE -> {
281                 unlockAnimator?.cancel()
282                 updateCurrentIntensity(effectTargetIntensity)
283                 lockStartTime = SystemClock.elapsedRealtime()
284                 animateWeatherIntensityOut(AUTO_FADE_DELAY_FROM_AWAY_MILLIS)
285             }
286             AnimationType.UNLOCK -> {
287                 // If already running, don't stop it.
288                 if (unlockAnimator?.isRunning == true) {
289                     return
290                 }
291 
292                 /*
293                  * When waking up the device (from AWAY), we normally wait for a delay
294                  * (AUTO_FADE_DELAY_FROM_AWAY_MILLIS) before playing the fade out animation.
295                  * However, there is a situation where this might be interrupted:
296                  *     AWAY -> LOCKED -> LOCKED -> ACTIVE.
297                  * If this happens, we might have already waited for sometime (between
298                  * AUTO_FADE_DELAY_MILLIS and AUTO_FADE_DELAY_FROM_AWAY_MILLIS). We compare how long
299                  * we've waited with AUTO_FADE_DELAY_MILLIS, and if we've waited longer than
300                  * AUTO_FADE_DELAY_MILLIS, we play the animation immediately. Otherwise, we wait
301                  * the rest of the AUTO_FADE_DELAY_MILLIS delay.
302                  */
303                 var delayTime = AUTO_FADE_DELAY_MILLIS
304                 if (unlockAnimator?.isStarted == true) {
305                     val deltaTime = (SystemClock.elapsedRealtime() - lockStartTime)
306                     delayTime = max(delayTime - deltaTime, 0)
307                     lockStartTime = 0
308                 }
309                 unlockAnimator?.cancel()
310                 updateCurrentIntensity()
311                 animateWeatherIntensityOut(delayTime, AUTO_FADE_SHORT_DURATION_MILLIS)
312             }
313             AnimationType.NONE -> {
314                 // No-op.
315             }
316         }
317     }
318 
319     private fun shouldSkipIntensityOutAnimation(): Boolean = isPreview() || isDebugActivity
320 
321     private fun animateWeatherIntensityOut(
322         delayMillis: Long,
323         durationMillis: Long = AUTO_FADE_DURATION_MILLIS,
324     ) {
325         unlockAnimator =
326             ValueAnimator.ofFloat(effectIntensity, 0f).apply {
327                 duration = durationMillis
328                 startDelay = delayMillis
329                 addUpdateListener { updatedAnimation ->
330                     effectIntensity = updatedAnimation.animatedValue as Float
331                     updateCurrentIntensity()
332                 }
333                 start()
334             }
335     }
336 
337     private fun getAnimationType(
338         newPresence: UserPresenceController.UserPresence,
339         oldPresence: UserPresenceController.UserPresence,
340     ): AnimationType {
341         if (shouldSkipIntensityOutAnimation()) {
342             return AnimationType.NONE
343         }
344         when (oldPresence) {
345             UserPresenceController.UserPresence.AWAY -> {
346                 if (
347                     newPresence == UserPresenceController.UserPresence.LOCKED ||
348                         newPresence == UserPresenceController.UserPresence.ACTIVE
349                 ) {
350                     return AnimationType.WAKE
351                 }
352             }
353             UserPresenceController.UserPresence.LOCKED -> {
354                 if (newPresence == UserPresenceController.UserPresence.ACTIVE) {
355                     return AnimationType.UNLOCK
356                 }
357             }
358             else -> {
359                 // No-op.
360             }
361         }
362 
363         return AnimationType.NONE
364     }
365 
366     private fun updateWallpaperColors(background: Bitmap) {
367         backgroundColor =
368             WallpaperColors.fromBitmap(
369                 Bitmap.createScaledBitmap(
370                     background,
371                     256,
372                     (background.width / background.height.toFloat() * 256).roundToInt(),
373                     /* filter = */ true,
374                 )
375             )
376     }
377 
378     /**
379      * Types of animations. Currently we animate when we wake the device (from screen off to lock
380      * screen or home screen) or when whe unlock device (from lock screen to home screen).
381      */
382     private enum class AnimationType {
383         UNLOCK,
384         WAKE,
385         NONE,
386     }
387 
388     private companion object {
389         private val TAG = WeatherEngine::class.java.simpleName
390 
391         private const val AUTO_FADE_DURATION_MILLIS: Long = 3000
392         private const val AUTO_FADE_SHORT_DURATION_MILLIS: Long = 3000
393         private const val AUTO_FADE_DELAY_MILLIS: Long = 1000
394         private const val AUTO_FADE_DELAY_FROM_AWAY_MILLIS: Long = 2000
395     }
396 }
397