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.
40  * An instance of this class should only implement [getWallpaperEngine]
41  *
42  * Note: [LiveWallpaper] subclasses must include the following attribute/s
43  * in the 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 WALLPAPER_FLAG_NOT_FOUND = -1
54     }
55 
56     // Holds the number of concurrent engines.
57     private var numEngines = 0
58 
59     // We can have multiple ConfigurationChangeListener because we can have multiple engines.
60     private val configChangeListeners: ArrayList<WeakReference<ConfigurationChangeListener>> =
61         ArrayList()
62 
63     // This is only needed for <= android R.
64     private val wakeStateChangeListeners: ArrayList<WeakReference<LiveWallpaperEngineWrapper>> =
65         ArrayList()
66     private lateinit var wakeStateReceiver: BroadcastReceiver
67 
onCreatenull68     override fun onCreate() {
69         super.onCreate()
70 
71         val wakeStateChangeIntentFilter = IntentFilter()
72         wakeStateChangeIntentFilter.addAction(Intent.ACTION_SCREEN_ON)
73         wakeStateChangeIntentFilter.addAction(Intent.ACTION_SCREEN_OFF)
74 
75         /*
76          * Only For Android R (SDK 30) or lower. Starting from S we can get wake/sleep events
77          * through WallpaperService.Engine.onCommand events that should be more accurate.
78          */
79         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
80             wakeStateReceiver = object : BroadcastReceiver() {
81                 override fun onReceive(context: Context, intent: Intent) {
82                     val positionExtras = Bundle()
83                     when (intent.action) {
84                         Intent.ACTION_SCREEN_ON -> {
85                             positionExtras.putInt(
86                                 LiveWallpaperEventListener.WAKE_ACTION_LOCATION_X,
87                                 -1
88                             )
89                             positionExtras.putInt(
90                                 LiveWallpaperEventListener.WAKE_ACTION_LOCATION_Y,
91                                 -1
92                             )
93                             wakeStateChangeListeners.forEach {
94                                 it.get()?.onWake(positionExtras)
95                             }
96                         }
97 
98                         Intent.ACTION_SCREEN_OFF -> {
99                             positionExtras.putInt(
100                                 LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_X,
101                                 -1
102                             )
103                             positionExtras.putInt(
104                                 LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_Y,
105                                 -1
106                             )
107                             wakeStateChangeListeners.forEach {
108                                 it.get()?.onSleep(positionExtras)
109                             }
110                         }
111                     }
112                 }
113             }
114             registerReceiver(wakeStateReceiver, wakeStateChangeIntentFilter)
115         }
116     }
117 
onDestroynull118     override fun onDestroy() {
119         super.onDestroy()
120         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) unregisterReceiver(wakeStateReceiver)
121     }
122 
123     /**
124      * Must be implemented to return a new instance of [TorusEngine].
125      * If you want it to subscribe to wallpaper interactions (offset, preview, zoom...) the engine
126      * should also implement [LiveWallpaperEventListener]. If you want it to subscribe to touch
127      * events, it should implement [TorusTouchListener].
128      *
129      * Note: You might have multiple Engines running at the same time (when the wallpaper is set as
130      * the active wallpaper and the user is in the wallpaper picker viewing a preview of it
131      * as well). You can track the lifecycle when *any* Engine is active using the
132      * is{First/Last}ActiveInstance parameters of the create/destroy methods.
133      *
134      */
getWallpaperEnginenull135     abstract fun getWallpaperEngine(context: Context, surfaceHolder: SurfaceHolder): TorusEngine
136 
137     /**
138      * returns a new instance of [LiveWallpaperEngineWrapper].
139      * Caution: This function should not be override when extending [LiveWallpaper] class.
140      */
141     override fun onCreateEngine(): Engine {
142         val wrapper = LiveWallpaperEngineWrapper()
143         wakeStateChangeListeners.add(WeakReference(wrapper))
144         return wrapper
145     }
146 
onConfigurationChangednull147     override fun onConfigurationChanged(newConfig: Configuration) {
148         super.onConfigurationChanged(newConfig)
149 
150         for (reference in configChangeListeners) {
151             reference.get()?.onConfigurationChanged(newConfig)
152         }
153     }
154 
addConfigChangeListenernull155     private fun addConfigChangeListener(configChangeListener: ConfigurationChangeListener) {
156         var containsListener = false
157 
158         for (reference in configChangeListeners) {
159             if (configChangeListener == reference.get()) {
160                 containsListener = true
161                 break
162             }
163         }
164 
165         if (!containsListener) {
166             configChangeListeners.add(WeakReference(configChangeListener))
167         }
168     }
169 
removeConfigChangeListenernull170     private fun removeConfigChangeListener(configChangeListener: ConfigurationChangeListener) {
171         for (reference in configChangeListeners) {
172             if (configChangeListener == reference.get()) {
173                 configChangeListeners.remove(reference)
174                 break
175             }
176         }
177     }
178 
179     /**
180      * Class that enables to connect a [TorusEngine] with some [WallpaperService.Engine] functions.
181      * The class that you use to render in a [LiveWallpaper] needs to inherit from
182      * [LiveWallpaperConnector] and implement [TorusEngine].
183      */
184     open class LiveWallpaperConnector {
185         private var wallpaperServiceEngine: WallpaperService.Engine? = null
186 
187         /**
188          * Returns the information if the wallpaper is in preview mode. This value doesn't change
189          * during a [TorusEngine] lifecycle, so you can know if the wallpaper is set checking that
190          * on create isPreview == false.
191          */
isPreviewnull192         fun isPreview(): Boolean {
193             this.wallpaperServiceEngine?.let {
194                 return it.isPreview
195             }
196             return false
197         }
198 
199         /**
200          * Triggers the [WallpaperService] to recompute the Wallpaper Colors.
201          */
notifyWallpaperColorsChangednull202         fun notifyWallpaperColorsChanged() {
203             this.wallpaperServiceEngine?.notifyColorsChanged()
204         }
205 
206         /** Returns the current Engine [SurfaceHolder]. */
getEngineSurfaceHoldernull207         fun getEngineSurfaceHolder(): SurfaceHolder? = this.wallpaperServiceEngine?.surfaceHolder
208 
209         /** Returns the wallpaper flags indicating which screen this Engine is rendering to. */
210         fun getWallpaperFlags(): Int {
211             if (Build.VERSION.SDK_INT >= 34) {
212                 this.wallpaperServiceEngine?.let {
213                     return it.wallpaperFlags
214                 }
215             }
216             return WALLPAPER_FLAG_NOT_FOUND
217         }
218 
setOffsetNotificationsEnablednull219         fun setOffsetNotificationsEnabled(enabled: Boolean) {
220             this.wallpaperServiceEngine?.setOffsetNotificationsEnabled(enabled)
221         }
222 
setServiceEngineReferencenull223         internal fun setServiceEngineReference(wallpaperServiceEngine: WallpaperService.Engine) {
224             this.wallpaperServiceEngine = wallpaperServiceEngine
225         }
226     }
227 
228     /**
229      * Implementation of [WallpaperService.Engine] that works as a wrapper. If we used a
230      * [WallpaperService.Engine] instance as the framework engine, we would find the problem
231      * that the engine will be created for preview, then destroyed and recreated again when the
232      * wallpaper is set. This behavior may cause to load assets multiple time for every time the
233      * Rendering engine is created. Also, wrapping our [TorusEngine] inside
234      * [WallpaperService.Engine] allow us to reuse [TorusEngine] in other places, like Activities.
235      */
236     private inner class LiveWallpaperEngineWrapper : WallpaperService.Engine() {
237         private lateinit var wallpaperEngine: TorusEngine
238 
onCreatenull239         override fun onCreate(surfaceHolder: SurfaceHolder) {
240             super.onCreate(surfaceHolder)
241             // Use RGBA_8888 format.
242             surfaceHolder.setFormat(PixelFormat.RGBA_8888)
243 
244             /*
245              * For Android 10 (SDK 29).
246              * This is needed for Foldables and multiple display devices.
247              */
248             val context = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
249                 displayContext ?: this@LiveWallpaper
250             } else {
251                 this@LiveWallpaper
252             }
253 
254             wallpaperEngine = getWallpaperEngine(context, surfaceHolder)
255             numEngines++
256 
257             /*
258              * It is important to call setTouchEventsEnabled in onCreate for it to work. Calling it
259              * in onSurfaceCreated instead will cause the engine to be stuck in an instantiation
260              * loop.
261              */
262             if (wallpaperEngine is TorusTouchListener) setTouchEventsEnabled(true)
263         }
264 
onSurfaceCreatednull265         override fun onSurfaceCreated(holder: SurfaceHolder) {
266             super.onSurfaceCreated(holder)
267 
268             if (wallpaperEngine is ConfigurationChangeListener) {
269                 addConfigChangeListener(wallpaperEngine as ConfigurationChangeListener)
270             }
271 
272             if (wallpaperEngine is LiveWallpaperConnector) {
273                 (wallpaperEngine as LiveWallpaperConnector).setServiceEngineReference(this)
274             }
275 
276             wallpaperEngine.create(numEngines == 1)
277         }
278 
onSurfaceDestroyednull279         override fun onSurfaceDestroyed(holder: SurfaceHolder?) {
280             super.onSurfaceDestroyed(holder)
281             numEngines--
282 
283             if (wallpaperEngine is ConfigurationChangeListener) {
284                 removeConfigChangeListener(wallpaperEngine as ConfigurationChangeListener)
285             }
286 
287             var isLastInstance = false
288             if (numEngines <= 0) {
289                 numEngines = 0
290                 isLastInstance = true
291             }
292 
293             if (isVisible) wallpaperEngine.pause()
294             wallpaperEngine.destroy(isLastInstance)
295         }
296 
onSurfaceChangednull297         override fun onSurfaceChanged(
298             holder: SurfaceHolder?,
299             format: Int,
300             width: Int,
301             height: Int
302         ) {
303             super.onSurfaceChanged(holder, format, width, height)
304             wallpaperEngine.resize(width, height)
305         }
306 
onOffsetsChangednull307         override fun onOffsetsChanged(
308             xOffset: Float,
309             yOffset: Float,
310             xOffsetStep: Float,
311             yOffsetStep: Float,
312             xPixelOffset: Int,
313             yPixelOffset: Int
314         ) {
315             super.onOffsetsChanged(
316                 xOffset,
317                 yOffset,
318                 xOffsetStep,
319                 yOffsetStep,
320                 xPixelOffset,
321                 yPixelOffset
322             )
323 
324             if (wallpaperEngine is LiveWallpaperEventListener) {
325                 (wallpaperEngine as LiveWallpaperEventListener).onOffsetChanged(
326                     xOffset,
327                     if (xOffsetStep.compareTo(0f) == 0) {
328                         1.0f
329                     } else {
330                         xOffsetStep
331                     }
332                 )
333             }
334         }
335 
onZoomChangednull336         override fun onZoomChanged(zoom: Float) {
337             super.onZoomChanged(zoom)
338             if (wallpaperEngine is LiveWallpaperEventListener) {
339                 (wallpaperEngine as LiveWallpaperEventListener).onZoomChanged(zoom)
340             }
341         }
342 
onVisibilityChangednull343         override fun onVisibilityChanged(visible: Boolean) {
344             super.onVisibilityChanged(visible)
345             if (visible) {
346                 wallpaperEngine.resume()
347             } else {
348                 wallpaperEngine.pause()
349             }
350         }
351 
onComputeColorsnull352         override fun onComputeColors(): WallpaperColors? {
353             if (wallpaperEngine is LiveWallpaperEventListener) {
354                 val colors =
355                     (wallpaperEngine as LiveWallpaperEventListener).computeWallpaperColors()
356 
357                 if (colors != null) {
358                     return colors
359                 }
360             }
361 
362             return super.onComputeColors()
363         }
364 
onCommandnull365         override fun onCommand(
366             action: String?,
367             x: Int,
368             y: Int,
369             z: Int,
370             extras: Bundle?,
371             resultRequested: Boolean
372         ): Bundle? {
373             when (action) {
374                 COMMAND_REAPPLY -> onWallpaperReapplied()
375                 COMMAND_WAKING_UP -> {
376                     val positionExtras = extras ?: Bundle()
377                     positionExtras.putInt(LiveWallpaperEventListener.WAKE_ACTION_LOCATION_X, x)
378                     positionExtras.putInt(LiveWallpaperEventListener.WAKE_ACTION_LOCATION_Y, y)
379                     onWake(positionExtras)
380                 }
381                 COMMAND_GOING_TO_SLEEP -> {
382                     val positionExtras = extras ?: Bundle()
383                     positionExtras.putInt(LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_X, x)
384                     positionExtras.putInt(LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_Y, y)
385                     onSleep(positionExtras)
386                 }
387                 COMMAND_KEYGUARD_GOING_AWAY -> onKeyguardGoingAway()
388                 COMMAND_PREVIEW_INFO -> onPreviewInfoReceived(extras)
389             }
390 
391             if (resultRequested) return extras
392 
393             return super.onCommand(action, x, y, z, extras, resultRequested)
394         }
395 
onTouchEventnull396         override fun onTouchEvent(event: MotionEvent) {
397             super.onTouchEvent(event)
398 
399             if (wallpaperEngine is TorusTouchListener) {
400                 (wallpaperEngine as TorusTouchListener).onTouchEvent(event)
401             }
402         }
403 
onWallpaperFlagsChangednull404         override fun onWallpaperFlagsChanged(which: Int) {
405             super.onWallpaperFlagsChanged(which)
406             wallpaperEngine.onWallpaperFlagsChanged(which)
407         }
408 
409         /**
410          * This is overriding a hidden API [WallpaperService.shouldZoomOutWallpaper].
411          */
shouldZoomOutWallpapernull412         fun shouldZoomOutWallpaper(): Boolean {
413             if (wallpaperEngine is LiveWallpaperEventListener) {
414                 return (wallpaperEngine as LiveWallpaperEventListener).shouldZoomOutWallpaper()
415             }
416             return false
417         }
418 
onWakenull419         fun onWake(extras: Bundle) {
420             if (wallpaperEngine is LiveWallpaperEventListener) {
421                 (wallpaperEngine as LiveWallpaperEventListener).onWake(extras)
422             }
423         }
424 
onSleepnull425         fun onSleep(extras: Bundle) {
426             if (wallpaperEngine is LiveWallpaperEventListener) {
427                 (wallpaperEngine as LiveWallpaperEventListener).onSleep(extras)
428             }
429         }
430 
onWallpaperReappliednull431         fun onWallpaperReapplied() {
432             if (wallpaperEngine is LiveWallpaperEventListener) {
433                 (wallpaperEngine as LiveWallpaperEventListener).onWallpaperReapplied()
434             }
435         }
436 
onKeyguardGoingAwaynull437         fun onKeyguardGoingAway() {
438             if (wallpaperEngine is LiveWallpaperKeyguardEventListener) {
439                 (wallpaperEngine as LiveWallpaperKeyguardEventListener).onKeyguardGoingAway()
440             }
441         }
442 
onPreviewInfoReceivednull443         fun onPreviewInfoReceived(extras: Bundle?) {
444             if (wallpaperEngine is LiveWallpaperEventListener) {
445                 (wallpaperEngine as LiveWallpaperEventListener).onPreviewInfoReceived(extras)
446             }
447         }
448     }
449 }
450