1 /*
2  * 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.torus.core.wallpaper
18 
19 import android.app.WallpaperColors
20 import android.content.BroadcastReceiver
21 import android.content.Context
22 import android.content.Intent
23 import android.content.IntentFilter
24 import android.content.res.Configuration
25 import android.graphics.PixelFormat
26 import android.os.Build
27 import android.os.Bundle
28 import android.service.wallpaper.WallpaperService
29 import android.view.MotionEvent
30 import android.view.SurfaceHolder
31 import com.google.android.torus.core.content.ConfigurationChangeListener
32 import com.google.android.torus.core.engine.TorusEngine
33 import com.google.android.torus.core.engine.listener.TorusTouchListener
34 import com.google.android.torus.core.wallpaper.listener.LiveWallpaperEventListener
35 import com.google.android.torus.core.wallpaper.listener.LiveWallpaperKeyguardEventListener
36 import java.lang.ref.WeakReference
37 
38 /**
39  * Implements [WallpaperService] using Filament to render the wallpaper. An instance of this class
40  * should only implement [getWallpaperEngine]
41  *
42  * Note: [LiveWallpaper] subclasses must include the following attribute/s in the
43  * AndroidManifest.xml:
44  * - android:configChanges="uiMode"
45  */
46 abstract class LiveWallpaper : WallpaperService() {
47     private companion object {
48         const val COMMAND_REAPPLY = "android.wallpaper.reapply"
49         const val COMMAND_WAKING_UP = "android.wallpaper.wakingup"
50         const val COMMAND_KEYGUARD_GOING_AWAY = "android.wallpaper.keyguardgoingaway"
51         const val COMMAND_GOING_TO_SLEEP = "android.wallpaper.goingtosleep"
52         const val COMMAND_PREVIEW_INFO = "android.wallpaper.previewinfo"
53         const val COMMAND_LOCKSCREEN_LAYOUT_CHANGED = "android.wallpaper.lockscreen_layout_changed"
54         const val WALLPAPER_FLAG_NOT_FOUND = -1
55     }
56 
57     // Holds the number of concurrent engines.
58     private var numEngines = 0
59 
60     // We can have multiple ConfigurationChangeListener because we can have multiple engines.
61     private val configChangeListeners: ArrayList<WeakReference<ConfigurationChangeListener>> =
62         ArrayList()
63 
64     // This is only needed for <= android R.
65     private val wakeStateChangeListeners: ArrayList<WeakReference<LiveWallpaperEngineWrapper>> =
66         ArrayList()
67     private lateinit var wakeStateReceiver: BroadcastReceiver
68 
onCreatenull69     override fun onCreate() {
70         super.onCreate()
71 
72         val wakeStateChangeIntentFilter = IntentFilter()
73         wakeStateChangeIntentFilter.addAction(Intent.ACTION_SCREEN_ON)
74         wakeStateChangeIntentFilter.addAction(Intent.ACTION_SCREEN_OFF)
75 
76         /*
77          * Only For Android R (SDK 30) or lower. Starting from S we can get wake/sleep events
78          * through WallpaperService.Engine.onCommand events that should be more accurate.
79          */
80         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
81             wakeStateReceiver =
82                 object : BroadcastReceiver() {
83                     override fun onReceive(context: Context, intent: Intent) {
84                         val positionExtras = Bundle()
85                         when (intent.action) {
86                             Intent.ACTION_SCREEN_ON -> {
87                                 positionExtras.putInt(
88                                     LiveWallpaperEventListener.WAKE_ACTION_LOCATION_X,
89                                     -1,
90                                 )
91                                 positionExtras.putInt(
92                                     LiveWallpaperEventListener.WAKE_ACTION_LOCATION_Y,
93                                     -1,
94                                 )
95                                 wakeStateChangeListeners.forEach {
96                                     it.get()?.onWake(positionExtras)
97                                 }
98                             }
99 
100                             Intent.ACTION_SCREEN_OFF -> {
101                                 positionExtras.putInt(
102                                     LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_X,
103                                     -1,
104                                 )
105                                 positionExtras.putInt(
106                                     LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_Y,
107                                     -1,
108                                 )
109                                 wakeStateChangeListeners.forEach {
110                                     it.get()?.onSleep(positionExtras)
111                                 }
112                             }
113                         }
114                     }
115                 }
116             registerReceiver(wakeStateReceiver, wakeStateChangeIntentFilter)
117         }
118     }
119 
onDestroynull120     override fun onDestroy() {
121         super.onDestroy()
122         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) unregisterReceiver(wakeStateReceiver)
123     }
124 
125     /**
126      * Must be implemented to return a new instance of [TorusEngine]. If you want it to subscribe to
127      * wallpaper interactions (offset, preview, zoom...) the engine should also implement
128      * [LiveWallpaperEventListener]. If you want it to subscribe to touch events, it should
129      * implement [TorusTouchListener].
130      *
131      * Note: You might have multiple Engines running at the same time (when the wallpaper is set as
132      * the active wallpaper and the user is in the wallpaper picker viewing a preview of it as
133      * well). You can track the lifecycle when *any* Engine is active using the
134      * is{First/Last}ActiveInstance parameters of the create/destroy methods.
135      */
getWallpaperEnginenull136     abstract fun getWallpaperEngine(context: Context, surfaceHolder: SurfaceHolder): TorusEngine
137 
138     /**
139      * returns a new instance of [LiveWallpaperEngineWrapper]. Caution: This function should not be
140      * override when extending [LiveWallpaper] class.
141      */
142     override fun onCreateEngine(): Engine {
143         val wrapper = LiveWallpaperEngineWrapper()
144         wakeStateChangeListeners.add(WeakReference(wrapper))
145         return wrapper
146     }
147 
onConfigurationChangednull148     override fun onConfigurationChanged(newConfig: Configuration) {
149         super.onConfigurationChanged(newConfig)
150 
151         for (reference in configChangeListeners) {
152             reference.get()?.onConfigurationChanged(newConfig)
153         }
154     }
155 
addConfigChangeListenernull156     private fun addConfigChangeListener(configChangeListener: ConfigurationChangeListener) {
157         var containsListener = false
158 
159         for (reference in configChangeListeners) {
160             if (configChangeListener == reference.get()) {
161                 containsListener = true
162                 break
163             }
164         }
165 
166         if (!containsListener) {
167             configChangeListeners.add(WeakReference(configChangeListener))
168         }
169     }
170 
removeConfigChangeListenernull171     private fun removeConfigChangeListener(configChangeListener: ConfigurationChangeListener) {
172         for (reference in configChangeListeners) {
173             if (configChangeListener == reference.get()) {
174                 configChangeListeners.remove(reference)
175                 break
176             }
177         }
178     }
179 
180     /**
181      * Class that enables to connect a [TorusEngine] with some [WallpaperService.Engine] functions.
182      * The class that you use to render in a [LiveWallpaper] needs to inherit from
183      * [LiveWallpaperConnector] and implement [TorusEngine].
184      */
185     open class LiveWallpaperConnector {
186         private var wallpaperServiceEngine: WallpaperService.Engine? = null
187 
188         /**
189          * Returns the information if the wallpaper is in preview mode. This value doesn't change
190          * during a [TorusEngine] lifecycle, so you can know if the wallpaper is set checking that
191          * on create isPreview == false.
192          */
isPreviewnull193         fun isPreview(): Boolean {
194             this.wallpaperServiceEngine?.let {
195                 return it.isPreview
196             }
197             return false
198         }
199 
200         /**
201          * Returns the information if the wallpaper is visible.
202          */
isVisiblenull203         fun isVisible(): Boolean {
204             this.wallpaperServiceEngine?.let {
205                 return it.isVisible
206             }
207             return false
208         }
209 
210         /** Triggers the [WallpaperService] to recompute the Wallpaper Colors. */
notifyWallpaperColorsChangednull211         fun notifyWallpaperColorsChanged() {
212             this.wallpaperServiceEngine?.notifyColorsChanged()
213         }
214 
215         /** Returns the current Engine [SurfaceHolder]. */
getEngineSurfaceHoldernull216         fun getEngineSurfaceHolder(): SurfaceHolder? = this.wallpaperServiceEngine?.surfaceHolder
217 
218         /** Returns the wallpaper flags indicating which screen this Engine is rendering to. */
219         fun getWallpaperFlags(): Int {
220             if (Build.VERSION.SDK_INT >= 34) {
221                 this.wallpaperServiceEngine?.let {
222                     return it.wallpaperFlags
223                 }
224             }
225             return WALLPAPER_FLAG_NOT_FOUND
226         }
227 
setOffsetNotificationsEnablednull228         fun setOffsetNotificationsEnabled(enabled: Boolean) {
229             this.wallpaperServiceEngine?.setOffsetNotificationsEnabled(enabled)
230         }
231 
setServiceEngineReferencenull232         internal fun setServiceEngineReference(wallpaperServiceEngine: WallpaperService.Engine) {
233             this.wallpaperServiceEngine = wallpaperServiceEngine
234         }
235     }
236 
237     /**
238      * Implementation of [WallpaperService.Engine] that works as a wrapper. If we used a
239      * [WallpaperService.Engine] instance as the framework engine, we would find the problem that
240      * the engine will be created for preview, then destroyed and recreated again when the wallpaper
241      * is set. This behavior may cause to load assets multiple time for every time the Rendering
242      * engine is created. Also, wrapping our [TorusEngine] inside [WallpaperService.Engine] allow us
243      * to reuse [TorusEngine] in other places, like Activities.
244      */
245     private inner class LiveWallpaperEngineWrapper : WallpaperService.Engine() {
246         private lateinit var wallpaperEngine: TorusEngine
247 
onCreatenull248         override fun onCreate(surfaceHolder: SurfaceHolder) {
249             super.onCreate(surfaceHolder)
250             // Use RGBA_8888 format.
251             surfaceHolder.setFormat(PixelFormat.RGBA_8888)
252 
253             /*
254              * For Android 10 (SDK 29).
255              * This is needed for Foldables and multiple display devices.
256              */
257             val context =
258                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
259                     displayContext ?: this@LiveWallpaper
260                 } else {
261                     this@LiveWallpaper
262                 }
263 
264             wallpaperEngine = getWallpaperEngine(context, surfaceHolder)
265             numEngines++
266 
267             /*
268              * It is important to call setTouchEventsEnabled in onCreate for it to work. Calling it
269              * in onSurfaceCreated instead will cause the engine to be stuck in an instantiation
270              * loop.
271              */
272             if (wallpaperEngine is TorusTouchListener) setTouchEventsEnabled(true)
273         }
274 
onSurfaceCreatednull275         override fun onSurfaceCreated(holder: SurfaceHolder) {
276             super.onSurfaceCreated(holder)
277 
278             if (wallpaperEngine is ConfigurationChangeListener) {
279                 addConfigChangeListener(wallpaperEngine as ConfigurationChangeListener)
280             }
281 
282             if (wallpaperEngine is LiveWallpaperConnector) {
283                 (wallpaperEngine as LiveWallpaperConnector).setServiceEngineReference(this)
284             }
285 
286             wallpaperEngine.create(numEngines == 1)
287         }
288 
onSurfaceDestroyednull289         override fun onSurfaceDestroyed(holder: SurfaceHolder?) {
290             super.onSurfaceDestroyed(holder)
291             numEngines--
292 
293             if (wallpaperEngine is ConfigurationChangeListener) {
294                 removeConfigChangeListener(wallpaperEngine as ConfigurationChangeListener)
295             }
296 
297             var isLastInstance = false
298             if (numEngines <= 0) {
299                 numEngines = 0
300                 isLastInstance = true
301             }
302 
303             if (isVisible) wallpaperEngine.pause()
304             wallpaperEngine.destroy(isLastInstance)
305         }
306 
onSurfaceChangednull307         override fun onSurfaceChanged(
308             holder: SurfaceHolder?,
309             format: Int,
310             width: Int,
311             height: Int,
312         ) {
313             super.onSurfaceChanged(holder, format, width, height)
314             wallpaperEngine.resize(width, height)
315         }
316 
onOffsetsChangednull317         override fun onOffsetsChanged(
318             xOffset: Float,
319             yOffset: Float,
320             xOffsetStep: Float,
321             yOffsetStep: Float,
322             xPixelOffset: Int,
323             yPixelOffset: Int,
324         ) {
325             super.onOffsetsChanged(
326                 xOffset,
327                 yOffset,
328                 xOffsetStep,
329                 yOffsetStep,
330                 xPixelOffset,
331                 yPixelOffset,
332             )
333 
334             if (wallpaperEngine is LiveWallpaperEventListener) {
335                 (wallpaperEngine as LiveWallpaperEventListener).onOffsetChanged(
336                     xOffset,
337                     if (xOffsetStep.compareTo(0f) == 0) {
338                         1.0f
339                     } else {
340                         xOffsetStep
341                     },
342                 )
343             }
344         }
345 
onZoomChangednull346         override fun onZoomChanged(zoom: Float) {
347             super.onZoomChanged(zoom)
348             if (wallpaperEngine is LiveWallpaperEventListener) {
349                 (wallpaperEngine as LiveWallpaperEventListener).onZoomChanged(zoom)
350             }
351         }
352 
onVisibilityChangednull353         override fun onVisibilityChanged(visible: Boolean) {
354             super.onVisibilityChanged(visible)
355             if (visible) {
356                 wallpaperEngine.resume()
357             } else {
358                 wallpaperEngine.pause()
359             }
360         }
361 
onComputeColorsnull362         override fun onComputeColors(): WallpaperColors? {
363             if (wallpaperEngine is LiveWallpaperEventListener) {
364                 val colors =
365                     (wallpaperEngine as LiveWallpaperEventListener).computeWallpaperColors()
366 
367                 if (colors != null) {
368                     return colors
369                 }
370             }
371 
372             return super.onComputeColors()
373         }
374 
onCommandnull375         override fun onCommand(
376             action: String?,
377             x: Int,
378             y: Int,
379             z: Int,
380             extras: Bundle?,
381             resultRequested: Boolean,
382         ): Bundle? {
383             when (action) {
384                 COMMAND_REAPPLY -> onWallpaperReapplied()
385                 COMMAND_WAKING_UP -> {
386                     val positionExtras = extras ?: Bundle()
387                     positionExtras.putInt(LiveWallpaperEventListener.WAKE_ACTION_LOCATION_X, x)
388                     positionExtras.putInt(LiveWallpaperEventListener.WAKE_ACTION_LOCATION_Y, y)
389                     onWake(positionExtras)
390                 }
391                 COMMAND_GOING_TO_SLEEP -> {
392                     val positionExtras = extras ?: Bundle()
393                     positionExtras.putInt(LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_X, x)
394                     positionExtras.putInt(LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_Y, y)
395                     onSleep(positionExtras)
396                 }
397                 COMMAND_KEYGUARD_GOING_AWAY -> onKeyguardGoingAway()
398                 COMMAND_PREVIEW_INFO -> onPreviewInfoReceived(extras)
399                 COMMAND_LOCKSCREEN_LAYOUT_CHANGED -> {
400                     if (extras != null) {
401                         onLockscreenLayoutChanged(extras)
402                     }
403                 }
404             }
405 
406             if (resultRequested) return extras
407 
408             return super.onCommand(action, x, y, z, extras, resultRequested)
409         }
410 
onTouchEventnull411         override fun onTouchEvent(event: MotionEvent) {
412             super.onTouchEvent(event)
413 
414             if (wallpaperEngine is TorusTouchListener) {
415                 (wallpaperEngine as TorusTouchListener).onTouchEvent(event)
416             }
417         }
418 
onWallpaperFlagsChangednull419         override fun onWallpaperFlagsChanged(which: Int) {
420             super.onWallpaperFlagsChanged(which)
421             wallpaperEngine.onWallpaperFlagsChanged(which)
422         }
423 
424         /** This is overriding a hidden API [WallpaperService.shouldZoomOutWallpaper]. */
shouldZoomOutWallpapernull425         override fun shouldZoomOutWallpaper(): Boolean {
426             if (wallpaperEngine is LiveWallpaperEventListener) {
427                 return (wallpaperEngine as LiveWallpaperEventListener).shouldZoomOutWallpaper()
428             }
429             return false
430         }
431 
onWakenull432         fun onWake(extras: Bundle) {
433             if (wallpaperEngine is LiveWallpaperEventListener) {
434                 (wallpaperEngine as LiveWallpaperEventListener).onWake(extras)
435             }
436         }
437 
onSleepnull438         fun onSleep(extras: Bundle) {
439             if (wallpaperEngine is LiveWallpaperEventListener) {
440                 (wallpaperEngine as LiveWallpaperEventListener).onSleep(extras)
441             }
442         }
443 
onWallpaperReappliednull444         fun onWallpaperReapplied() {
445             if (wallpaperEngine is LiveWallpaperEventListener) {
446                 (wallpaperEngine as LiveWallpaperEventListener).onWallpaperReapplied()
447             }
448         }
449 
onKeyguardGoingAwaynull450         fun onKeyguardGoingAway() {
451             if (wallpaperEngine is LiveWallpaperKeyguardEventListener) {
452                 (wallpaperEngine as LiveWallpaperKeyguardEventListener).onKeyguardGoingAway()
453             }
454         }
455 
onPreviewInfoReceivednull456         fun onPreviewInfoReceived(extras: Bundle?) {
457             if (wallpaperEngine is LiveWallpaperEventListener) {
458                 (wallpaperEngine as LiveWallpaperEventListener).onPreviewInfoReceived(extras)
459             }
460         }
461 
onLockscreenLayoutChangednull462         fun onLockscreenLayoutChanged(extras: Bundle) {
463             if (wallpaperEngine is LiveWallpaperEventListener) {
464                 (wallpaperEngine as LiveWallpaperEventListener).onLockscreenLayoutChanged(extras)
465             }
466         }
467     }
468 }
469