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