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