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 }