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