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