1 /* 2 * Copyright (C) 2022 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.torus.canvas.engine 18 19 import android.graphics.Canvas 20 import android.graphics.RuntimeShader 21 import android.os.SystemClock 22 import android.util.Log 23 import android.util.Size 24 import android.view.Choreographer 25 import android.view.SurfaceHolder 26 import androidx.annotation.VisibleForTesting 27 import com.google.android.torus.core.engine.TorusEngine 28 import com.google.android.torus.core.power.FpsThrottler 29 import com.google.android.torus.core.time.TimeController 30 import com.google.android.torus.core.wallpaper.LiveWallpaper 31 import java.io.PrintWriter 32 33 /** 34 * Class that implements [TorusEngine] using Canvas and can be used in a [LiveWallpaper]. This 35 * class also inherits from [LiveWallpaper.LiveWallpaperConnector] which allows to do some calls 36 * related to Live Wallpapers, like the method [isPreview] or [notifyWallpaperColorsChanged]. 37 * 38 * By default it won't start [startUpdateLoop]. To run animations and update logic per frame, call 39 * [startUpdateLoop] and [stopUpdateLoop] when it's no longer needed. 40 * 41 * This class also can be used with the new RuntimeShader. 42 */ 43 abstract class CanvasWallpaperEngine( 44 /** The default [SurfaceHolder] to be used. */ 45 private val defaultHolder: SurfaceHolder, 46 47 /** 48 * Defines if the surface should be hardware accelerated or not. If you are using 49 * [RuntimeShader], this value should be set to true. When setting it to true, some 50 * functions might not be supported. Please refer to the documentation: 51 * https://developer.android.com/guide/topics/graphics/hardware-accel#unsupported 52 */ 53 private val hardwareAccelerated: Boolean = false, 54 ) : LiveWallpaper.LiveWallpaperConnector(), TorusEngine { 55 56 private val choreographer = Choreographer.getInstance() <lambda>null57 private val timeController = TimeController().also { 58 it.resetDeltaTime(SystemClock.uptimeMillis()) 59 } 60 private val frameScheduler = FrameCallback() 61 private val fpsThrottler = FpsThrottler() 62 63 protected var screenSize = Size(0, 0) 64 private set 65 private var resizeCalled: Boolean = false 66 67 private var isWallpaperEngineVisible = false 68 /** 69 * Indicates whether the engine#onCreate is called. 70 * 71 * TODO(b/277672928): These two booleans were introduced as a workaround where 72 * [onSurfaceRedrawNeeded] called after an [onSurfaceDestroyed], without [onCreate]/ 73 * [onSurfaceCreated] being called between those. Remove these once it's fixed in 74 * [WallpaperService]. 75 */ 76 private var isCreated = false 77 private var shouldInvokeResume = false 78 79 /** Callback to handle when the [TorusEngine] has been created. */ 80 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) onCreatenull81 open fun onCreate(isFirstActiveInstance: Boolean) { 82 // No-op. Ready for being overridden by children. 83 } 84 85 /** Callback to handle when the [TorusEngine] has been resumed. */ 86 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) onResumenull87 open fun onResume() { 88 // No-op. Ready for being overridden by children. 89 } 90 91 /** Callback to handle when the [TorusEngine] has been paused. */ 92 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) onPausenull93 open fun onPause() { 94 // No-op. Ready for being overridden by children. 95 } 96 97 /** 98 * Callback to handle when the surface holding the [TorusEngine] has changed its size. 99 * 100 * @param size The new size of the surface holding the [TorusEngine]. 101 */ 102 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) onResizenull103 open fun onResize(size: Size) { 104 // No-op. Ready for being overridden by children. 105 } 106 107 /** 108 * Callback to handle when the [TorusEngine] needs to be updated. Call [startUpdateLoop] to 109 * initiate the frame loop; call [stopUpdateLoop] to end the loop. The client is supposed to 110 * update logic and render in this loop. 111 * 112 * @param deltaMillis The time in millis since the last time [onUpdate] was called. 113 * @param frameTimeNanos The time in nanoseconds when the frame started being rendered, 114 * in the [System.nanoTime] timebase. 115 */ 116 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) onUpdatenull117 open fun onUpdate(deltaMillis: Long, frameTimeNanos: Long) { 118 // No-op. Ready for being overridden by children. 119 } 120 121 /** 122 * Callback to handle when we need to destroy the surface. 123 * 124 * @param isLastActiveInstance Whether this was the last wallpaper engine instance (until the 125 * next [onCreate]). 126 */ 127 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) onDestroynull128 open fun onDestroy(isLastActiveInstance: Boolean) { 129 // No-op. Ready for being overridden by children. 130 } 131 createnull132 final override fun create(isFirstActiveInstance: Boolean) { 133 screenSize = Size( 134 getCurrentSurfaceHolder().surfaceFrame.width(), 135 getCurrentSurfaceHolder().surfaceFrame.height() 136 ) 137 138 onCreate(isFirstActiveInstance) 139 140 isCreated = true 141 142 if (shouldInvokeResume) { 143 Log.e( 144 TAG, "Force invoke resume. onVisibilityChanged must have been called" + 145 "before onCreate.") 146 resume() 147 shouldInvokeResume = false 148 } 149 } 150 pausenull151 final override fun pause() { 152 if (!isCreated) { 153 Log.e( 154 TAG, "Engine is not yet created but pause is called. Set a flag to invoke" + 155 " resume on next create.") 156 shouldInvokeResume = true 157 return 158 } 159 160 if (isWallpaperEngineVisible) { 161 onPause() 162 isWallpaperEngineVisible = false 163 } 164 } 165 resumenull166 final override fun resume() { 167 if (!isCreated) { 168 Log.e( 169 TAG, "Engine is not yet created but resume is called. Set a flag to " + 170 "invoke resume on next create.") 171 shouldInvokeResume = true 172 return 173 } 174 175 if (!isWallpaperEngineVisible) { 176 onResume() 177 isWallpaperEngineVisible = true 178 } 179 } 180 resizenull181 final override fun resize(width: Int, height: Int) { 182 resizeCalled = true 183 184 screenSize = Size(width, height) 185 onResize(screenSize) 186 } 187 destroynull188 final override fun destroy(isLastActiveInstance: Boolean) { 189 choreographer.removeFrameCallback(frameScheduler) 190 timeController.resetDeltaTime(SystemClock.uptimeMillis()) 191 192 // Always detach the surface before destroying the engine 193 onDestroy(isLastActiveInstance) 194 } 195 196 /** 197 * Renders to canvas. Use this in [onUpdate] loop. This will automatically throttle (or limit) 198 * FPS that was set via [setFpsLimit]. 199 * 200 * @param frameTimeNanos The time in nanoseconds when the frame started being rendered, in the 201 * [System.nanoTime] timebase. 202 * @param onRender The callback triggered when the canvas is ready for render. 203 * 204 * @return Whether it is rendered. 205 */ renderWithFpsLimitnull206 fun renderWithFpsLimit(frameTimeNanos: Long, onRender: (canvas: Canvas) -> Unit): Boolean { 207 if (resizeCalled) { 208 /** 209 * Skip rendering a frame to a buffer with potentially-outdated dimensions, and request 210 * redraw in the next frame. 211 */ 212 resizeCalled = false 213 214 fpsThrottler.requestRendering() 215 return renderWithFpsLimit(frameTimeNanos, onRender) 216 } 217 218 return fpsThrottler.tryRender(frameTimeNanos) { 219 renderToCanvas(onRender) 220 } 221 } 222 223 /** 224 * Renders to canvas. 225 * 226 * @param onRender The callback triggered when the canvas is ready for render. 227 * 228 * @return Whether it is rendered. 229 */ rendernull230 fun render(onRender: (canvas: Canvas) -> Unit): Boolean { 231 if (resizeCalled) { 232 /** 233 * Skip rendering a frame to a buffer with potentially-outdated dimensions, and request 234 * redraw in the next frame. 235 */ 236 resizeCalled = false 237 return render(onRender) 238 } 239 240 return renderToCanvas(onRender) 241 } 242 243 /** 244 * Sets the FPS limit. See [FpsThrottler] for the FPS constants. The max FPS will be the screen 245 * refresh (VSYNC) rate. 246 * 247 * @param fps Desired mas FPS. 248 */ setFpsLimitnull249 protected fun setFpsLimit(fps: Float) { 250 fpsThrottler.updateFps(fps) 251 } 252 253 /** 254 * Starts the update loop. 255 */ startUpdateLoopnull256 protected fun startUpdateLoop() { 257 if (!frameScheduler.running) { 258 frameScheduler.running = true 259 choreographer.postFrameCallback(frameScheduler) 260 } 261 } 262 263 /** 264 * Stops the update loop. 265 */ stopUpdateLoopnull266 protected fun stopUpdateLoop() { 267 if (frameScheduler.running) { 268 frameScheduler.running = false 269 choreographer.removeFrameCallback(frameScheduler) 270 } 271 } 272 renderToCanvasnull273 private fun renderToCanvas(onRender: (canvas: Canvas) -> Unit): Boolean { 274 val surfaceHolder = getCurrentSurfaceHolder() 275 if (!surfaceHolder.surface.isValid) return false 276 var canvas: Canvas? = null 277 278 try { 279 canvas = if (hardwareAccelerated) { 280 surfaceHolder.lockHardwareCanvas() 281 } else { 282 surfaceHolder.lockCanvas() 283 } ?: return false 284 285 onRender(canvas) 286 287 } catch (e: java.lang.Exception) { 288 Log.e("canvas_exception", "canvas exception", e) 289 } finally { 290 if (canvas != null) { 291 surfaceHolder.unlockCanvasAndPost(canvas) 292 } 293 } 294 return true 295 } 296 getCurrentSurfaceHoldernull297 private fun getCurrentSurfaceHolder(): SurfaceHolder = 298 getEngineSurfaceHolder() ?: defaultHolder 299 300 /** 301 * Implementation of [Choreographer.FrameCallback] which triggers [onUpdate]. 302 */ 303 inner class FrameCallback : Choreographer.FrameCallback { 304 internal var running: Boolean = false 305 306 override fun doFrame(frameTimeNanos: Long) { 307 if (running) choreographer.postFrameCallback(this) 308 // onUpdate should be called for every V_SYNC. 309 val frameTimeMillis = frameTimeNanos / 1000_000 310 timeController.updateDeltaTime(frameTimeMillis) 311 onUpdate(timeController.deltaTimeMillis, frameTimeNanos) 312 timeController.resetDeltaTime(frameTimeMillis) 313 } 314 } 315 316 /** 317 * Override this for dumpsys. 318 * 319 * You still need to have your WallpaperService overriding [dump] and call 320 * [CanvasWallpaperEngine.dump]. 321 * 322 * Usage: adb shell dumpsys activity service ${your_wallpaper_service_name}. 323 */ dumpnull324 open fun dump(out: PrintWriter) = Unit 325 326 private companion object { 327 private val TAG: String = CanvasWallpaperEngine::class.java.simpleName 328 } 329 }