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