Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit df0e4b39 authored by Michel Comin Escude's avatar Michel Comin Escude
Browse files

Fade out weather effects after duration time

Done as described in go/weather-effects-specification.

Flag: EXEMPT MP apk not in build yet
Bug: 352352241
Test: visual
Change-Id: I74e3840fdb7b9c3b1f280b565f5c5e13576f2941
parent b687bd4d
Loading
Loading
Loading
Loading
+11 −5
Original line number Diff line number Diff line
@@ -41,11 +41,11 @@ import com.google.android.wallpaper.weathereffects.data.repository.WallpaperFile
import com.google.android.wallpaper.weathereffects.domain.WeatherEffectsInteractor
import com.google.android.wallpaper.weathereffects.provider.WallpaperInfoContract
import com.google.android.wallpaper.weathereffects.shared.model.WallpaperFileModel
import java.io.File
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
import javax.inject.Inject

class WallpaperEffectsDebugActivity : TorusViewerActivity() {

@@ -73,7 +73,13 @@ class WallpaperEffectsDebugActivity : TorusViewerActivity() {

    override fun getWallpaperEngine(context: Context, surfaceView: SurfaceView): TorusEngine {
        this.surfaceView = surfaceView
        val engine = WeatherEngine(surfaceView.holder, mainScope, interactor, context)
        val engine = WeatherEngine(
            surfaceView.holder,
            mainScope,
            interactor,
            context,
            isDebugActivity = true
        )
        this.engine = engine
        return engine
    }
@@ -150,7 +156,7 @@ class WallpaperEffectsDebugActivity : TorusViewerActivity() {
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                // Convert progress to a value between 0 and 1
                val value = progress.toFloat() / 100f
                engine?.setIntensity(value)
                engine?.setTargetIntensity(value)
                intensity = value
            }

@@ -216,7 +222,7 @@ class WallpaperEffectsDebugActivity : TorusViewerActivity() {
                    weatherEffect,
                )
            )
            engine?.setIntensity(intensity)
            engine?.setTargetIntensity(intensity)
            setDebugText(
                "Wallpaper updated successfully.\n* Weather: " +
                        "$weatherEffect\n* Foreground: $fgPath\n* Background: $bgPath"
+211 −10
Original line number Diff line number Diff line
@@ -16,15 +16,20 @@

package com.google.android.wallpaper.weathereffects

import android.animation.ValueAnimator
import android.app.WallpaperColors
import android.content.Context
import android.graphics.Bitmap
import android.os.Bundle
import android.os.SystemClock
import android.util.Log
import android.util.Size
import android.util.SizeF
import android.view.SurfaceHolder
import androidx.annotation.FloatRange
import com.google.android.torus.canvas.engine.CanvasWallpaperEngine
import com.google.android.wallpaper.weathereffects.shared.model.WallpaperImageModel
import com.google.android.torus.core.wallpaper.listener.LiveWallpaperEventListener
import com.google.android.torus.core.wallpaper.listener.LiveWallpaperKeyguardEventListener
import com.google.android.wallpaper.weathereffects.domain.WeatherEffectsInteractor
import com.google.android.wallpaper.weathereffects.graphics.WeatherEffect
import com.google.android.wallpaper.weathereffects.graphics.fog.FogEffect
@@ -35,18 +40,28 @@ import com.google.android.wallpaper.weathereffects.graphics.rain.RainEffectConfi
import com.google.android.wallpaper.weathereffects.graphics.snow.SnowEffect
import com.google.android.wallpaper.weathereffects.graphics.snow.SnowEffectConfig
import com.google.android.wallpaper.weathereffects.provider.WallpaperInfoContract
import com.google.android.wallpaper.weathereffects.sensor.UserPresenceController
import com.google.android.wallpaper.weathereffects.shared.model.WallpaperImageModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.math.max
import kotlin.math.roundToInt

class WeatherEngine(
    defaultHolder: SurfaceHolder,
    private val applicationScope: CoroutineScope,
    private val interactor: WeatherEffectsInteractor,
    private val context: Context,
    private val isDebugActivity: Boolean = false,
    hardwareAccelerated: Boolean = true
) : CanvasWallpaperEngine(defaultHolder, hardwareAccelerated) {
) : CanvasWallpaperEngine(defaultHolder, hardwareAccelerated), LiveWallpaperKeyguardEventListener,
    LiveWallpaperEventListener {

    private var lockStartTime: Long = 0
    private var unlockAnimator: ValueAnimator? = null

    private var backgroundColor: WallpaperColors? = null
    private var currentAssets: WallpaperImageModel? = null
    private var activeEffect: WeatherEffect? = null
        private set(value) {
@@ -59,7 +74,13 @@ class WeatherEngine(
        }

    private var collectWallpaperImageJob: Job? = null
    private var effectIntensity: Float = 1f
    private var effectTargetIntensity: Float = 1f
    private var effectIntensity: Float = 0f

    private var userPresenceController =
        UserPresenceController(context) { newUserPresence, oldUserPresence ->
            onUserPresenceChange(newUserPresence, oldUserPresence)
        }

    init {
        /* Load assets. */
@@ -72,6 +93,16 @@ class WeatherEngine(

    override fun onCreate(isFirstActiveInstance: Boolean) {
        Log.d(TAG, "Engine created.")
        /*
         * Initialize `effectIntensity` to `effectTargetIntensity` so we show the weather effect
         * on preview and when `isDebugActivity` is true.
         *
         * isPreview() is only reliable after `onCreate`. Thus update the initial value of
         * `effectIntensity` in case it is not 0.
         */
        if (shouldSkipIntensityOutAnimation()) {
            updateCurrentIntensity(effectTargetIntensity)
        }
    }

    override fun onResize(size: Size) {
@@ -87,13 +118,16 @@ class WeatherEngine(
                if (asset == null || asset == currentAssets) return@collect
                currentAssets = asset
                createWeatherEffect(asset.foreground, asset.background, asset.weatherEffect)
                updateWallpaperColors(asset.background)
            }
        }
        if (activeEffect != null) {
            if (shouldTriggerUpdate()) startUpdateLoop()
        }
        userPresenceController.start(context.mainExecutor)
    }

    override fun onUpdate(deltaMillis: Long, frameTimeNanos: Long) {
        super.onUpdate(deltaMillis, frameTimeNanos)
        activeEffect?.update(deltaMillis, frameTimeNanos)

        renderWithFpsLimit(frameTimeNanos) { canvas -> activeEffect?.draw(canvas) }
@@ -101,8 +135,9 @@ class WeatherEngine(

    override fun onPause() {
        stopUpdateLoop()
        activeEffect?.reset()
        collectWallpaperImageJob?.cancel()
        activeEffect?.reset()
        userPresenceController.stop()
    }

    override fun onDestroy(isLastActiveInstance: Boolean) {
@@ -110,9 +145,41 @@ class WeatherEngine(
        activeEffect = null
    }

    fun setIntensity(@FloatRange(from = 0.0, to = 1.0) intensity: Float) {
        effectIntensity = intensity
        activeEffect?.setIntensity(intensity)
    override fun onKeyguardGoingAway() {
        userPresenceController.onKeyguardGoingAway()
    }

    override fun onOffsetChanged(xOffset: Float, xOffsetStep: Float) {
        // No-op.
    }

    override fun onZoomChanged(zoomLevel: Float) {
        // No-op.
    }

    override fun onWallpaperReapplied() {
        // No-op.
    }

    override fun shouldZoomOutWallpaper(): Boolean = true

    override fun computeWallpaperColors(): WallpaperColors? = backgroundColor

    override fun onWake(extras: Bundle) {
        userPresenceController.setWakeState(true)
    }

    override fun onSleep(extras: Bundle) {
        userPresenceController.setWakeState(false)
    }

    fun setTargetIntensity(@FloatRange(from = 0.0, to = 1.0) intensity: Float) {
        effectTargetIntensity = intensity

        /* If we don't want to animate, update the target intensity as it happens. */
        if (shouldSkipIntensityOutAnimation()) {
            updateCurrentIntensity(effectTargetIntensity)
        }
    }

    private fun createWeatherEffect(
@@ -146,6 +213,8 @@ class WeatherEngine(
            }
        }

        updateCurrentIntensity()

        render { canvas -> activeEffect?.draw(canvas) }
    }

@@ -155,8 +224,140 @@ class WeatherEngine(

    private fun Size.toSizeF(): SizeF = SizeF(width.toFloat(), height.toFloat())

    private companion object {
    private fun onUserPresenceChange(
        newUserPresence: UserPresenceController.UserPresence,
        oldUserPresence: UserPresenceController.UserPresence
    ) {
        playIntensityFadeOutAnimation(
            getAnimationType(newUserPresence, oldUserPresence)
        )
    }

    private fun updateCurrentIntensity(intensity: Float = effectIntensity) {
        if (effectIntensity != intensity) {
            effectIntensity = intensity
        }
        activeEffect?.setIntensity(effectIntensity)
    }

    private fun playIntensityFadeOutAnimation(animationType: AnimationType) {
        when (animationType) {
            AnimationType.WAKE -> {
                unlockAnimator?.cancel()
                updateCurrentIntensity(effectTargetIntensity)
                lockStartTime = SystemClock.elapsedRealtime()
                animateWeatherIntensityOut(AUTO_FADE_DELAY_FROM_AWAY_MILLIS)
            }

            AnimationType.UNLOCK -> {
                // If already running, don't stop it.
                if (unlockAnimator?.isRunning == true) {
                    return
                }

                /*
                 * When waking up the device (from AWAY), we normally wait for a delay
                 * (AUTO_FADE_DELAY_FROM_AWAY_MILLIS) before playing the fade out animation.
                 * However, there is a situation where this might be interrupted:
                 *     AWAY -> LOCKED -> LOCKED -> ACTIVE.
                 * If this happens, we might have already waited for sometime (between
                 * AUTO_FADE_DELAY_MILLIS and AUTO_FADE_DELAY_FROM_AWAY_MILLIS). We compare how long
                 * we've waited with AUTO_FADE_DELAY_MILLIS, and if we've waited longer than
                 * AUTO_FADE_DELAY_MILLIS, we play the animation immediately. Otherwise, we wait
                 * the rest of the AUTO_FADE_DELAY_MILLIS delay.
                 */
                var delayTime = AUTO_FADE_DELAY_MILLIS
                if (unlockAnimator?.isStarted == true) {
                    val deltaTime = (SystemClock.elapsedRealtime() - lockStartTime)
                    delayTime = max(delayTime - deltaTime, 0)
                    lockStartTime = 0
                }
                unlockAnimator?.cancel()
                updateCurrentIntensity()
                animateWeatherIntensityOut(delayTime, AUTO_FADE_SHORT_DURATION_MILLIS)
            }

            AnimationType.NONE -> {
                // No-op.
            }
        }
    }

    private fun shouldSkipIntensityOutAnimation(): Boolean = isPreview() || isDebugActivity

    private fun animateWeatherIntensityOut(
        delayMillis: Long,
        durationMillis: Long = AUTO_FADE_DURATION_MILLIS
    ) {
        unlockAnimator = ValueAnimator.ofFloat(effectIntensity, 0f).apply {
            duration = durationMillis
            startDelay = delayMillis
            addUpdateListener { updatedAnimation ->
                effectIntensity = updatedAnimation.animatedValue as Float
                updateCurrentIntensity()
            }
            start()
        }
    }

    private fun getAnimationType(
        newPresence: UserPresenceController.UserPresence,
        oldPresence: UserPresenceController.UserPresence
    ): AnimationType {
        if (shouldSkipIntensityOutAnimation()) {
            return AnimationType.NONE
        }
        when (oldPresence) {
            UserPresenceController.UserPresence.AWAY -> {
                if (
                    newPresence == UserPresenceController.UserPresence.LOCKED ||
                    newPresence == UserPresenceController.UserPresence.ACTIVE
                ) {
                    return AnimationType.WAKE
                }
            }

            UserPresenceController.UserPresence.LOCKED -> {
                if (newPresence == UserPresenceController.UserPresence.ACTIVE) {
                    return AnimationType.UNLOCK
                }
            }

            else -> {
                // No-op.
            }
        }

        return AnimationType.NONE
    }

    private fun updateWallpaperColors(background: Bitmap) {
        backgroundColor = WallpaperColors.fromBitmap(
            Bitmap.createScaledBitmap(
                background,
                256,
                (background.width / background.height.toFloat() * 256).roundToInt(),
                /* filter = */ true
            )
        )
    }

    /**
     * Types of animations. Currently we animate when we wake the device (from screen off to lock
     * screen or home screen) or when whe unlock device (from lock screen to home screen).
     */
    private enum class AnimationType {
        UNLOCK,
        WAKE,
        NONE
    }

    private companion object {
        private val TAG = WeatherEngine::class.java.simpleName

        private const val AUTO_FADE_DURATION_MILLIS: Long = 3000
        private const val AUTO_FADE_SHORT_DURATION_MILLIS: Long = 3000
        private const val AUTO_FADE_DELAY_MILLIS: Long = 1000
        private const val AUTO_FADE_DELAY_FROM_AWAY_MILLIS: Long = 2000
    }
}
+6 −6
Original line number Diff line number Diff line
@@ -69,6 +69,12 @@ class UserPresenceController(
        updateUserPresence(isDeviceAwake = isAwake)
    }

    /**
     * Call when the keyguard is going away. This will happen before lock state is false (but it
     * happens at the same time that unlock animation starts).
     */
    fun onKeyguardGoingAway() = updateUserPresence(isDeviceLocked = false)

    private fun updateUserPresence(
        isDeviceAwake: Boolean = deviceAwake,
        isDeviceLocked: Boolean = deviceLocked
@@ -83,12 +89,6 @@ class UserPresenceController(
        }
    }

    /**
     * Call when the keyguard is going away. This will happen before lock state is false (but it
     * happens at the same time that unlock animation starts).
     */
    fun onKeyguardGoingAway() = updateUserPresence(isDeviceLocked = false)

    /** Define the different user presence available. */
    enum class UserPresence {