• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 onKeyguardAppearing() {}
156 
157     override fun onOffsetChanged(xOffset: Float, xOffsetStep: Float) {
158         // No-op.
159     }
160 
161     override fun onZoomChanged(zoomLevel: Float) {
162         // No-op.
163     }
164 
165     override fun onWallpaperReapplied() {
166         // No-op.
167     }
168 
169     override fun shouldZoomOutWallpaper(): Boolean = true
170 
171     override fun computeWallpaperColors(): WallpaperColors? = backgroundColor
172 
173     override fun onWake(extras: Bundle) {
174         userPresenceController.setWakeState(true)
175     }
176 
177     override fun onSleep(extras: Bundle) {
178         userPresenceController.setWakeState(false)
179     }
180 
181     fun setTargetIntensity(@FloatRange(from = 0.0, to = 1.0) intensity: Float) {
182         effectTargetIntensity = intensity
183 
184         /* If we don't want to animate, update the target intensity as it happens. */
185         if (shouldSkipIntensityOutAnimation()) {
186             updateCurrentIntensity(effectTargetIntensity)
187         }
188     }
189 
190     private fun createWeatherEffect(
191         foreground: Bitmap,
192         background: Bitmap,
193         weatherEffect: WallpaperInfoContract.WeatherEffect? = null,
194     ) {
195         activeEffect?.release()
196         activeEffect = null
197 
198         when (weatherEffect) {
199             WallpaperInfoContract.WeatherEffect.RAIN -> {
200                 val rainConfig =
201                     RainEffectConfig(context.assets, context.resources.displayMetrics.density)
202                 activeEffect =
203                     RainEffect(
204                         rainConfig,
205                         foreground,
206                         background,
207                         effectIntensity,
208                         screenSize.toSizeF(),
209                         context.mainExecutor,
210                     )
211             }
212             WallpaperInfoContract.WeatherEffect.FOG -> {
213                 val fogConfig =
214                     FogEffectConfig(context.assets, context.resources.displayMetrics.density)
215 
216                 activeEffect =
217                     FogEffect(
218                         fogConfig,
219                         foreground,
220                         background,
221                         effectIntensity,
222                         screenSize.toSizeF(),
223                     )
224             }
225             WallpaperInfoContract.WeatherEffect.SNOW -> {
226                 val snowConfig =
227                     SnowEffectConfig(context.assets, context.resources.displayMetrics.density)
228                 activeEffect =
229                     SnowEffect(
230                         snowConfig,
231                         foreground,
232                         background,
233                         effectIntensity,
234                         screenSize.toSizeF(),
235                         context.mainExecutor,
236                     )
237             }
238             WallpaperInfoContract.WeatherEffect.SUN -> {
239                 val snowConfig =
240                     SunEffectConfig(context.assets, context.resources.displayMetrics.density)
241                 activeEffect =
242                     SunEffect(
243                         snowConfig,
244                         foreground,
245                         background,
246                         effectIntensity,
247                         screenSize.toSizeF(),
248                     )
249             }
250             else -> {
251                 activeEffect = NoEffect(foreground, background, screenSize.toSizeF())
252             }
253         }
254 
255         updateCurrentIntensity()
256 
257         render { canvas -> activeEffect?.draw(canvas) }
258     }
259 
260     private fun shouldTriggerUpdate(): Boolean {
261         return activeEffect != null && activeEffect !is NoEffect
262     }
263 
264     private fun Size.toSizeF(): SizeF = SizeF(width.toFloat(), height.toFloat())
265 
266     private fun onUserPresenceChange(
267         newUserPresence: UserPresenceController.UserPresence,
268         oldUserPresence: UserPresenceController.UserPresence,
269     ) {
270         playIntensityFadeOutAnimation(getAnimationType(newUserPresence, oldUserPresence))
271     }
272 
273     private fun updateCurrentIntensity(intensity: Float = effectIntensity) {
274         if (effectIntensity != intensity) {
275             effectIntensity = intensity
276         }
277         activeEffect?.setIntensity(effectIntensity)
278     }
279 
280     private fun playIntensityFadeOutAnimation(animationType: AnimationType) {
281         when (animationType) {
282             AnimationType.WAKE -> {
283                 unlockAnimator?.cancel()
284                 updateCurrentIntensity(effectTargetIntensity)
285                 lockStartTime = SystemClock.elapsedRealtime()
286                 animateWeatherIntensityOut(AUTO_FADE_DELAY_FROM_AWAY_MILLIS)
287             }
288             AnimationType.UNLOCK -> {
289                 // If already running, don't stop it.
290                 if (unlockAnimator?.isRunning == true) {
291                     return
292                 }
293 
294                 /*
295                  * When waking up the device (from AWAY), we normally wait for a delay
296                  * (AUTO_FADE_DELAY_FROM_AWAY_MILLIS) before playing the fade out animation.
297                  * However, there is a situation where this might be interrupted:
298                  *     AWAY -> LOCKED -> LOCKED -> ACTIVE.
299                  * If this happens, we might have already waited for sometime (between
300                  * AUTO_FADE_DELAY_MILLIS and AUTO_FADE_DELAY_FROM_AWAY_MILLIS). We compare how long
301                  * we've waited with AUTO_FADE_DELAY_MILLIS, and if we've waited longer than
302                  * AUTO_FADE_DELAY_MILLIS, we play the animation immediately. Otherwise, we wait
303                  * the rest of the AUTO_FADE_DELAY_MILLIS delay.
304                  */
305                 var delayTime = AUTO_FADE_DELAY_MILLIS
306                 if (unlockAnimator?.isStarted == true) {
307                     val deltaTime = (SystemClock.elapsedRealtime() - lockStartTime)
308                     delayTime = max(delayTime - deltaTime, 0)
309                     lockStartTime = 0
310                 }
311                 unlockAnimator?.cancel()
312                 updateCurrentIntensity()
313                 animateWeatherIntensityOut(delayTime, AUTO_FADE_SHORT_DURATION_MILLIS)
314             }
315             AnimationType.NONE -> {
316                 // No-op.
317             }
318         }
319     }
320 
321     private fun shouldSkipIntensityOutAnimation(): Boolean = isPreview() || isDebugActivity
322 
323     private fun animateWeatherIntensityOut(
324         delayMillis: Long,
325         durationMillis: Long = AUTO_FADE_DURATION_MILLIS,
326     ) {
327         unlockAnimator =
328             ValueAnimator.ofFloat(effectIntensity, 0f).apply {
329                 duration = durationMillis
330                 startDelay = delayMillis
331                 addUpdateListener { updatedAnimation ->
332                     effectIntensity = updatedAnimation.animatedValue as Float
333                     updateCurrentIntensity()
334                 }
335                 start()
336             }
337     }
338 
339     private fun getAnimationType(
340         newPresence: UserPresenceController.UserPresence,
341         oldPresence: UserPresenceController.UserPresence,
342     ): AnimationType {
343         if (shouldSkipIntensityOutAnimation()) {
344             return AnimationType.NONE
345         }
346         when (oldPresence) {
347             UserPresenceController.UserPresence.AWAY -> {
348                 if (
349                     newPresence == UserPresenceController.UserPresence.LOCKED ||
350                         newPresence == UserPresenceController.UserPresence.ACTIVE
351                 ) {
352                     return AnimationType.WAKE
353                 }
354             }
355             UserPresenceController.UserPresence.LOCKED -> {
356                 if (newPresence == UserPresenceController.UserPresence.ACTIVE) {
357                     return AnimationType.UNLOCK
358                 }
359             }
360             else -> {
361                 // No-op.
362             }
363         }
364 
365         return AnimationType.NONE
366     }
367 
368     private fun updateWallpaperColors(background: Bitmap) {
369         backgroundColor =
370             WallpaperColors.fromBitmap(
371                 Bitmap.createScaledBitmap(
372                     background,
373                     256,
374                     (background.width / background.height.toFloat() * 256).roundToInt(),
375                     /* filter = */ true,
376                 )
377             )
378     }
379 
380     /**
381      * Types of animations. Currently we animate when we wake the device (from screen off to lock
382      * screen or home screen) or when whe unlock device (from lock screen to home screen).
383      */
384     private enum class AnimationType {
385         UNLOCK,
386         WAKE,
387         NONE,
388     }
389 
390     private companion object {
391         private val TAG = WeatherEngine::class.java.simpleName
392 
393         private const val AUTO_FADE_DURATION_MILLIS: Long = 3000
394         private const val AUTO_FADE_SHORT_DURATION_MILLIS: Long = 3000
395         private const val AUTO_FADE_DELAY_MILLIS: Long = 1000
396         private const val AUTO_FADE_DELAY_FROM_AWAY_MILLIS: Long = 2000
397     }
398 }
399