diff options
Diffstat (limited to 'toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/LiveWallpaper.kt')
-rw-r--r-- | toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/LiveWallpaper.kt | 441 |
1 files changed, 441 insertions, 0 deletions
diff --git a/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/LiveWallpaper.kt b/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/LiveWallpaper.kt new file mode 100644 index 0000000..9a5d96e --- /dev/null +++ b/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/LiveWallpaper.kt @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.torus.core.wallpaper + +import android.app.WallpaperColors +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.res.Configuration +import android.graphics.PixelFormat +import android.os.Build +import android.os.Bundle +import android.service.wallpaper.WallpaperService +import android.view.MotionEvent +import android.view.SurfaceHolder +import androidx.annotation.RequiresApi +import com.google.android.torus.core.content.ConfigurationChangeListener +import com.google.android.torus.core.engine.TorusEngine +import com.google.android.torus.core.engine.listener.TorusTouchListener +import com.google.android.torus.core.wallpaper.listener.LiveWallpaperEventListener +import com.google.android.torus.core.wallpaper.listener.LiveWallpaperKeyguardEventListener +import java.lang.ref.WeakReference + +/** + * Implements [WallpaperService] using Filament to render the wallpaper. + * An instance of this class should only implement [getWallpaperEngine] + * + * Note: [LiveWallpaper] subclasses must include the following attribute/s + * in the AndroidManifest.xml: + * - android:configChanges="uiMode" + */ +abstract class LiveWallpaper : WallpaperService() { + private companion object { + const val COMMAND_REAPPLY = "android.wallpaper.reapply" + const val COMMAND_WAKING_UP = "android.wallpaper.wakingup" + const val COMMAND_KEYGUARD_GOING_AWAY = "android.wallpaper.keyguardgoingaway" + const val COMMAND_GOING_TO_SLEEP = "android.wallpaper.goingtosleep" + const val COMMAND_PREVIEW_INFO = "android.wallpaper.previewinfo" + const val WALLPAPER_FLAG_NOT_FOUND = -1 + } + + // Holds the number of concurrent engines. + private var numEngines = 0 + + // We can have multiple ConfigurationChangeListener because we can have multiple engines. + private val configChangeListeners: ArrayList<WeakReference<ConfigurationChangeListener>> = + ArrayList() + + // This is only needed for <= android R. + private val wakeStateChangeListeners: ArrayList<WeakReference<LiveWallpaperEngineWrapper>> = + ArrayList() + private lateinit var wakeStateReceiver: BroadcastReceiver + + override fun onCreate() { + super.onCreate() + + val wakeStateChangeIntentFilter = IntentFilter() + wakeStateChangeIntentFilter.addAction(Intent.ACTION_SCREEN_ON) + wakeStateChangeIntentFilter.addAction(Intent.ACTION_SCREEN_OFF) + + /* + * Only For Android R (SDK 30) or lower. Starting from S we can get wake/sleep events + * through WallpaperService.Engine.onCommand events that should be more accurate. + */ + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + wakeStateReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val positionExtras = Bundle() + when (intent.action) { + Intent.ACTION_SCREEN_ON -> { + positionExtras.putInt( + LiveWallpaperEventListener.WAKE_ACTION_LOCATION_X, + -1 + ) + positionExtras.putInt( + LiveWallpaperEventListener.WAKE_ACTION_LOCATION_Y, + -1 + ) + wakeStateChangeListeners.forEach { + it.get()?.onWake(positionExtras) + } + } + + Intent.ACTION_SCREEN_OFF -> { + positionExtras.putInt( + LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_X, + -1 + ) + positionExtras.putInt( + LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_Y, + -1 + ) + wakeStateChangeListeners.forEach { + it.get()?.onSleep(positionExtras) + } + } + } + } + } + registerReceiver(wakeStateReceiver, wakeStateChangeIntentFilter) + } + } + + override fun onDestroy() { + super.onDestroy() + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) unregisterReceiver(wakeStateReceiver) + } + + /** + * Must be implemented to return a new instance of [TorusEngine]. + * If you want it to subscribe to wallpaper interactions (offset, preview, zoom...) the engine + * should also implement [LiveWallpaperEventListener]. If you want it to subscribe to touch + * events, it should implement [TorusTouchListener]. + * + * Note: You might have multiple Engines running at the same time (when the wallpaper is set as + * the active wallpaper and the user is in the wallpaper picker viewing a preview of it + * as well). You can track the lifecycle when *any* Engine is active using the + * is{First/Last}ActiveInstance parameters of the create/destroy methods. + * + */ + abstract fun getWallpaperEngine(context: Context, surfaceHolder: SurfaceHolder): TorusEngine + + /** + * returns a new instance of [LiveWallpaperEngineWrapper]. + * Caution: This function should not be override when extending [LiveWallpaper] class. + */ + override fun onCreateEngine(): Engine { + val wrapper = LiveWallpaperEngineWrapper() + wakeStateChangeListeners.add(WeakReference(wrapper)) + return wrapper + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + for (reference in configChangeListeners) { + reference.get()?.onConfigurationChanged(newConfig) + } + } + + private fun addConfigChangeListener(configChangeListener: ConfigurationChangeListener) { + var containsListener = false + + for (reference in configChangeListeners) { + if (configChangeListener == reference.get()) { + containsListener = true + break + } + } + + if (!containsListener) { + configChangeListeners.add(WeakReference(configChangeListener)) + } + } + + private fun removeConfigChangeListener(configChangeListener: ConfigurationChangeListener) { + for (reference in configChangeListeners) { + if (configChangeListener == reference.get()) { + configChangeListeners.remove(reference) + break + } + } + } + + /** + * Class that enables to connect a [TorusEngine] with some [WallpaperService.Engine] functions. + * The class that you use to render in a [LiveWallpaper] needs to inherit from + * [LiveWallpaperConnector] and implement [TorusEngine]. + */ + open class LiveWallpaperConnector { + private var wallpaperServiceEngine: WallpaperService.Engine? = null + + /** + * Returns the information if the wallpaper is in preview mode. This value doesn't change + * during a [TorusEngine] lifecycle, so you can know if the wallpaper is set checking that + * on create isPreview == false. + */ + fun isPreview(): Boolean { + this.wallpaperServiceEngine?.let { + return it.isPreview + } + return false + } + + /** + * Triggers the [WallpaperService] to recompute the Wallpaper Colors. + */ + fun notifyWallpaperColorsChanged() { + this.wallpaperServiceEngine?.notifyColorsChanged() + } + + /** Returns the current Engine [SurfaceHolder]. */ + fun getEngineSurfaceHolder(): SurfaceHolder? = this.wallpaperServiceEngine?.surfaceHolder + + /** Returns the wallpaper flags indicating which screen this Engine is rendering to. */ + fun getWallpaperFlags(): Int { + if (Build.VERSION.SDK_INT >= 34) { + this.wallpaperServiceEngine?.let { + return it.wallpaperFlags + } + } + return WALLPAPER_FLAG_NOT_FOUND + } + + internal fun setServiceEngineReference(wallpaperServiceEngine: WallpaperService.Engine) { + this.wallpaperServiceEngine = wallpaperServiceEngine + } + } + + /** + * Implementation of [WallpaperService.Engine] that works as a wrapper. If we used a + * [WallpaperService.Engine] instance as the framework engine, we would find the problem + * that the engine will be created for preview, then destroyed and recreated again when the + * wallpaper is set. This behavior may cause to load assets multiple time for every time the + * Rendering engine is created. Also, wrapping our [TorusEngine] inside + * [WallpaperService.Engine] allow us to reuse [TorusEngine] in other places, like Activities. + */ + private inner class LiveWallpaperEngineWrapper : WallpaperService.Engine() { + private lateinit var wallpaperEngine: TorusEngine + + override fun onCreate(surfaceHolder: SurfaceHolder) { + super.onCreate(surfaceHolder) + // Use RGBA_8888 format. + surfaceHolder.setFormat(PixelFormat.RGBA_8888) + + /* + * For Android 10 (SDK 29). + * This is needed for Foldables and multiple display devices. + */ + val context = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + displayContext ?: this@LiveWallpaper + } else { + this@LiveWallpaper + } + + wallpaperEngine = getWallpaperEngine(context, surfaceHolder) + numEngines++ + + /* + * It is important to call setTouchEventsEnabled in onCreate for it to work. Calling it + * in onSurfaceCreated instead will cause the engine to be stuck in an instantiation + * loop. + */ + if (wallpaperEngine is TorusTouchListener) setTouchEventsEnabled(true) + } + + override fun onSurfaceCreated(holder: SurfaceHolder) { + super.onSurfaceCreated(holder) + + if (wallpaperEngine is ConfigurationChangeListener) { + addConfigChangeListener(wallpaperEngine as ConfigurationChangeListener) + } + + if (wallpaperEngine is LiveWallpaperConnector) { + (wallpaperEngine as LiveWallpaperConnector).setServiceEngineReference(this) + } + + wallpaperEngine.create(numEngines == 1) + } + + override fun onSurfaceDestroyed(holder: SurfaceHolder?) { + super.onSurfaceDestroyed(holder) + numEngines-- + + if (wallpaperEngine is ConfigurationChangeListener) { + removeConfigChangeListener(wallpaperEngine as ConfigurationChangeListener) + } + + var isLastInstance = false + if (numEngines <= 0) { + numEngines = 0 + isLastInstance = true + } + + if (isVisible) wallpaperEngine.pause() + wallpaperEngine.destroy(isLastInstance) + } + + override fun onSurfaceChanged( + holder: SurfaceHolder?, + format: Int, + width: Int, + height: Int + ) { + super.onSurfaceChanged(holder, format, width, height) + wallpaperEngine.resize(width, height) + } + + override fun onOffsetsChanged( + xOffset: Float, + yOffset: Float, + xOffsetStep: Float, + yOffsetStep: Float, + xPixelOffset: Int, + yPixelOffset: Int + ) { + super.onOffsetsChanged( + xOffset, + yOffset, + xOffsetStep, + yOffsetStep, + xPixelOffset, + yPixelOffset + ) + + if (wallpaperEngine is LiveWallpaperEventListener) { + (wallpaperEngine as LiveWallpaperEventListener).onOffsetChanged( + xOffset, + if (xOffsetStep.compareTo(0f) == 0) { + 1.0f + } else { + xOffsetStep + } + ) + } + } + + override fun onZoomChanged(zoom: Float) { + super.onZoomChanged(zoom) + if (wallpaperEngine is LiveWallpaperEventListener) { + (wallpaperEngine as LiveWallpaperEventListener).onZoomChanged(zoom) + } + } + + override fun onVisibilityChanged(visible: Boolean) { + super.onVisibilityChanged(visible) + if (visible) { + wallpaperEngine.resume() + } else { + wallpaperEngine.pause() + } + } + + override fun onComputeColors(): WallpaperColors? { + if (wallpaperEngine is LiveWallpaperEventListener) { + val colors = + (wallpaperEngine as LiveWallpaperEventListener).computeWallpaperColors() + + if (colors != null) { + return colors + } + } + + return super.onComputeColors() + } + + override fun onCommand( + action: String?, + x: Int, + y: Int, + z: Int, + extras: Bundle?, + resultRequested: Boolean + ): Bundle? { + when (action) { + COMMAND_REAPPLY -> onWallpaperReapplied() + COMMAND_WAKING_UP -> { + val positionExtras = extras ?: Bundle() + positionExtras.putInt(LiveWallpaperEventListener.WAKE_ACTION_LOCATION_X, x) + positionExtras.putInt(LiveWallpaperEventListener.WAKE_ACTION_LOCATION_Y, y) + onWake(positionExtras) + } + COMMAND_GOING_TO_SLEEP -> { + val positionExtras = extras ?: Bundle() + positionExtras.putInt(LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_X, x) + positionExtras.putInt(LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_Y, y) + onSleep(positionExtras) + } + COMMAND_KEYGUARD_GOING_AWAY -> onKeyguardGoingAway() + COMMAND_PREVIEW_INFO -> onPreviewInfoReceived(extras) + } + + if (resultRequested) return extras + + return super.onCommand(action, x, y, z, extras, resultRequested) + } + + override fun onTouchEvent(event: MotionEvent) { + super.onTouchEvent(event) + + if (wallpaperEngine is TorusTouchListener) { + (wallpaperEngine as TorusTouchListener).onTouchEvent(event) + } + } + + /** + * This is overriding a hidden API [WallpaperService.shouldZoomOutWallpaper]. + */ + fun shouldZoomOutWallpaper(): Boolean { + if (wallpaperEngine is LiveWallpaperEventListener) { + return (wallpaperEngine as LiveWallpaperEventListener).shouldZoomOutWallpaper() + } + return false + } + + fun onWake(extras: Bundle) { + if (wallpaperEngine is LiveWallpaperEventListener) { + (wallpaperEngine as LiveWallpaperEventListener).onWake(extras) + } + } + + fun onSleep(extras: Bundle) { + if (wallpaperEngine is LiveWallpaperEventListener) { + (wallpaperEngine as LiveWallpaperEventListener).onSleep(extras) + } + } + + fun onWallpaperReapplied() { + if (wallpaperEngine is LiveWallpaperEventListener) { + (wallpaperEngine as LiveWallpaperEventListener).onWallpaperReapplied() + } + } + + fun onKeyguardGoingAway() { + if (wallpaperEngine is LiveWallpaperKeyguardEventListener) { + (wallpaperEngine as LiveWallpaperKeyguardEventListener).onKeyguardGoingAway() + } + } + + fun onPreviewInfoReceived(extras: Bundle?) { + if (wallpaperEngine is LiveWallpaperEventListener) { + (wallpaperEngine as LiveWallpaperEventListener).onPreviewInfoReceived(extras) + } + } + } +} |