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