Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- `startWallpaperRotation`, `stopWallpaperRotation`, `rotateWallpaperNow`, and `getWallpaperRotationStatus` now return an `unsupported` result / a not-running status off Android instead of falling through to a platform call.
- `getWallpaperRotationStatus()` no longer throws when the platform call fails; it returns a not-running status with `lastError` set instead.
- OpenGL live wallpaper: an unexpected I/O error while reading a texture source during configuration is now reported as `texture-source-unavailable` instead of `configuration-store-failed`, matching what the renderer reports for the same failure.
- Video and OpenGL live wallpapers now reload when the user sets the plugin's wallpaper again over itself. Android keeps the running engine for the same component and only sends it a reapply command, which the engines ignored, so the home screen kept the previous video or shader until the process restarted. On Android 10 and older the running engine picks up the new content only when its surface is recreated.
- Internal: removed unused legacy platform-channel endpoints and dead pre-Android 7 code paths.

## 3.2.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.app.WallpaperManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.os.SystemClock
Expand Down Expand Up @@ -200,6 +201,21 @@ class OpenGlLiveWallpaper : WallpaperService() {
super.onTouchEvent(event)
}

/** Loads the newly saved configuration when the user confirms this same component again. */
override fun onCommand(
action: String?,
x: Int,
y: Int,
z: Int,
extras: Bundle?,
resultRequested: Boolean,
): Bundle? {
if (action == WALLPAPER_COMMAND_REAPPLY) {
renderThread.configurationChanged(OpenGlWallpaperConfigurationStore.load(applicationContext))
}
return super.onCommand(action, x, y, z, extras, resultRequested)
}

override fun onDestroy() {
renderThread.shutdown()
super.onDestroy()
Expand All @@ -208,21 +224,16 @@ class OpenGlLiveWallpaper : WallpaperService() {

private class WallpaperRenderThread(
context: Context,
private val configuration: OpenGlWallpaperConfiguration,
initialConfiguration: OpenGlWallpaperConfiguration,
) {
private val appContext = context.applicationContext
private val thread = HandlerThread("AsyncWallpaper-OpenGL").apply { start() }
private val handler = Handler(thread.looper)
private val frameIntervalMillis = max(
1L,
1_000L / configuration.frameRate.coerceIn(
ShaderProgramValidator.MIN_FRAME_RATE.toInt(),
ShaderProgramValidator.MAX_FRAME_RATE.toInt(),
),
)
private val createdAtNanos = SystemClock.elapsedRealtimeNanos()

// Everything below is accessed only from [handler].
private var configuration = initialConfiguration
private var frameIntervalMillis = frameIntervalFor(initialConfiguration)
private var destroyed = false
private var visible = false
private var surface: Surface? = null
Expand Down Expand Up @@ -326,6 +337,23 @@ class OpenGlLiveWallpaper : WallpaperService() {
}
}

/** Swaps in a newly confirmed configuration; the next frame builds a renderer for it. */
fun configurationChanged(newConfiguration: OpenGlWallpaperConfiguration) {
handler.post {
if (destroyed) {
return@post
}
configuration = newConfiguration
frameIntervalMillis = frameIntervalFor(newConfiguration)
renderer?.release()
renderer = null
attachedGeneration = -1L
fatalSurfaceGeneration = null
invalidateScheduledFrame()
scheduleFrame(immediate = true)
}
}

fun shutdown() {
if (Thread.currentThread() === thread) {
releaseOnRenderThread()
Expand Down Expand Up @@ -445,6 +473,14 @@ class OpenGlLiveWallpaper : WallpaperService() {
private companion object {
private const val NANOS_PER_SECOND = 1_000_000_000.0
private const val SHUTDOWN_WAIT_MILLIS = 1_000L

private fun frameIntervalFor(configuration: OpenGlWallpaperConfiguration): Long = max(
1L,
1_000L / configuration.frameRate.coerceIn(
ShaderProgramValidator.MIN_FRAME_RATE.toInt(),
ShaderProgramValidator.MAX_FRAME_RATE.toInt(),
),
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.media.MediaPlayer
import android.os.Bundle
import android.service.wallpaper.WallpaperService
import android.util.Log
import android.view.SurfaceHolder
import java.io.File

/**
* `WallpaperManager.COMMAND_REAPPLY` (hidden API). On API 30+ Android does not rebind a live
* wallpaper when the user sets the same component again; it sends this command to the running
* engine instead, so the engine must reload its content itself.
*/
internal const val WALLPAPER_COMMAND_REAPPLY = "android.wallpaper.reapply"

/** The two scaling modes supported directly by Android's MediaPlayer. */
enum class VideoWallpaperScaleMode {
CENTER_CROP,
Expand Down Expand Up @@ -61,6 +69,26 @@ class VideoLiveWallpaper : WallpaperService() {
super.onDestroy()
}

/** Reloads the active video when the user confirms this same component again. */
override fun onCommand(
action: String?,
x: Int,
y: Int,
z: Int,
extras: Bundle?,
resultRequested: Boolean,
): Bundle? {
if (action == WALLPAPER_COMMAND_REAPPLY) {
synchronized(lifecycleLock) {
if (stateMachine.onAssetReapplied() == VideoPlaybackAction.PREPARE) {
releasePlayerLocked()
preparePlayerLocked(surfaceHolder)
}
}
}
return super.onCommand(action, x, y, z, extras, resultRequested)
}

private fun preparePlayerLocked(holder: SurfaceHolder) {
val file = File(filesDir, VideoWallpaperRepository.ACTIVE_VIDEO_FILE_NAME)
if (!file.isFile || !file.canRead() || file.length() <= 0L) {
Expand Down Expand Up @@ -269,6 +297,15 @@ class VideoPlaybackStateMachine {
}
}

/** A new active asset was confirmed; replace the player if a surface can show it now. */
fun onAssetReapplied(): VideoPlaybackAction {
if (state == VideoPlaybackState.RELEASED || !hasSurface) {
return VideoPlaybackAction.NONE
}
state = VideoPlaybackState.PREPARING
return VideoPlaybackAction.PREPARE
}

fun onPlayerError(): VideoPlaybackAction {
if (state == VideoPlaybackState.RELEASED) {
return VideoPlaybackAction.NONE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,33 @@ class VideoStateMachineTest {
assertEquals(VideoPlaybackAction.START, stateMachine.onVisibilityChanged(true))
}

@Test
fun `reapply replaces the player only while a surface can show it`() {
val stateMachine = VideoPlaybackStateMachine()
assertEquals(VideoPlaybackAction.NONE, stateMachine.onAssetReapplied())

stateMachine.onSurfaceCreated()
stateMachine.onVisibilityChanged(true)
stateMachine.onPrepared()
assertEquals(VideoPlaybackState.PLAYING, stateMachine.state)
assertEquals(VideoPlaybackAction.PREPARE, stateMachine.onAssetReapplied())
assertEquals(VideoPlaybackState.PREPARING, stateMachine.state)
assertEquals(VideoPlaybackAction.START, stateMachine.onPrepared())

stateMachine.onPlayerError()
assertEquals(VideoPlaybackAction.PREPARE, stateMachine.onAssetReapplied())

stateMachine.onSurfaceDestroyed()
assertEquals(VideoPlaybackAction.NONE, stateMachine.onAssetReapplied())
stateMachine.onEngineDestroyed()
assertEquals(VideoPlaybackAction.NONE, stateMachine.onAssetReapplied())
}

@Test
fun `reapply command matches the platform constant`() {
assertEquals("android.wallpaper.reapply", WALLPAPER_COMMAND_REAPPLY)
}

@Test
fun `hidden preparation waits until wallpaper is visible`() {
val stateMachine = VideoPlaybackStateMachine()
Expand Down
1 change: 1 addition & 0 deletions doc/android-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ Consequently:
- `previewOpened` means that the preview UI opened, not that the wallpaper was applied.
- `awaitingUserConfirmation` means Android is waiting for the user's choice.
- This release reports `previewOpened` when it launches the system UI and does not infer `applied` after the user leaves that UI.
- When the plugin's live wallpaper is already active and the user sets it again with new content, Android 11+ keeps the running engine and the plugin reloads the content in place. On Android 10 and older the new content appears only after the wallpaper's surface is recreated, for example after a restart.
- `cancelled`, `unsupported`, and `failed` must remain visible to the user/app; do not replace them with a success toast.

This limitation is documented rather than hidden because Android does not expose a portable API to force a live-wallpaper target across OEM preview implementations.
Expand Down
Loading