feat(wallpaper): add apply_wallpaper workflow and rotation worker
Adds a new apply_wallpaper command and its plumbing across desktop, mobile, and Android. A WorkManager-based rotation worker handles applying wallpapers in the background. Wallpaper state now tracks currentId and imageIds for precise control. - Introduced apply_wallpaper command across desktop, mobile and Android - Added WorkManager-based rotation worker to apply wallpapers in the background - Extended wallpaper state with currentId and imageIds and updated schemas/permissions
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<application>
|
||||
<service android:name="de.wechselbild.wallpaper.WallpaperRotationService" android:exported="false" android:foregroundServiceType="specialUse">
|
||||
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Changes the user-selected lock-screen wallpaper at the configured interval" />
|
||||
|
||||
@@ -3,7 +3,6 @@ package de.wechselbild.wallpaper
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class WallpaperAlarmReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
@@ -11,24 +10,10 @@ class WallpaperAlarmReceiver : BroadcastReceiver() {
|
||||
WallpaperScheduler.cancel(context)
|
||||
return
|
||||
}
|
||||
val applicationContext = context.applicationContext
|
||||
val result = goAsync()
|
||||
executor.execute {
|
||||
try {
|
||||
if (WallpaperScheduler.claimIfDue(applicationContext)) try {
|
||||
WallpaperStore.applyNext(applicationContext)
|
||||
} finally {
|
||||
WallpaperScheduler.scheduleNext(applicationContext)
|
||||
}
|
||||
} finally {
|
||||
WallpaperScheduler.ensureScheduled(applicationContext)
|
||||
WallpaperRotationService.rescheduleRunningService()
|
||||
result.finish()
|
||||
}
|
||||
if (WallpaperScheduler.claimIfDue(context)) {
|
||||
WallpaperRotationWorker.enqueue(context)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
WallpaperScheduler.ensureScheduled(context)
|
||||
WallpaperRotationService.rescheduleRunningService()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ class GalleryArgs { var offset: Int = 0; var limit: Int = 48 }
|
||||
@InvokeArg
|
||||
class DeleteImageArgs { lateinit var id: String }
|
||||
|
||||
@InvokeArg
|
||||
class ApplyWallpaperArgs { lateinit var id: String }
|
||||
|
||||
@InvokeArg
|
||||
class DeleteImagesArgs { var ids: Array<String> = emptyArray() }
|
||||
|
||||
@@ -130,7 +133,7 @@ class WallpaperPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
try {
|
||||
val args = invoke.parseArgs(SettingArgs::class.java)
|
||||
WallpaperStore.set(activity, args.name, args.value)
|
||||
invoke.resolve(WallpaperStore.state(activity, includePreviews = false))
|
||||
invoke.resolve(WallpaperStore.state(activity))
|
||||
} catch (error: Exception) { invoke.reject(error.message ?: "Einstellung konnte nicht gespeichert werden") }
|
||||
}
|
||||
|
||||
@@ -139,7 +142,7 @@ class WallpaperPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
val args = invoke.parseArgs(IntervalArgs::class.java)
|
||||
WallpaperStore.setInterval(activity, args.minutes)
|
||||
if (args.minutes > 0) WallpaperRotationService.restart(activity) else WallpaperRotationService.stop(activity)
|
||||
invoke.resolve(WallpaperStore.state(activity, includePreviews = false))
|
||||
invoke.resolve(WallpaperStore.state(activity))
|
||||
} catch (error: Exception) { invoke.reject(error.message ?: "Wechselintervall konnte nicht gespeichert werden") }
|
||||
}
|
||||
|
||||
@@ -150,6 +153,14 @@ class WallpaperPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
} catch (error: Exception) { invoke.reject(error.message ?: "Das Hintergrundbild konnte nicht geändert werden") }
|
||||
}
|
||||
|
||||
@Command fun applyWallpaper(invoke: Invoke) = io.execute {
|
||||
try {
|
||||
val args = invoke.parseArgs(ApplyWallpaperArgs::class.java)
|
||||
if (WallpaperStore.apply(activity, args.id)) invoke.resolve(WallpaperStore.state(activity))
|
||||
else invoke.reject("Bild konnte nicht angewendet werden")
|
||||
} catch (error: Exception) { invoke.reject(error.message ?: "Das Hintergrundbild konnte nicht geändert werden") }
|
||||
}
|
||||
|
||||
@Command fun getImmichConnection(invoke: Invoke) = io.execute {
|
||||
invoke.resolve(ImmichClient.connection(activity))
|
||||
}
|
||||
|
||||
@@ -24,10 +24,8 @@ class WallpaperRotationService : Service() {
|
||||
return
|
||||
}
|
||||
worker.execute {
|
||||
if (WallpaperScheduler.claimIfDue(this@WallpaperRotationService)) try {
|
||||
WallpaperStore.applyNext(this@WallpaperRotationService)
|
||||
} finally {
|
||||
WallpaperScheduler.scheduleNext(this@WallpaperRotationService)
|
||||
if (WallpaperScheduler.claimIfDue(this@WallpaperRotationService)) {
|
||||
WallpaperRotationWorker.enqueue(this@WallpaperRotationService)
|
||||
}
|
||||
handler.post { scheduleLocalTimer() }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.wechselbild.wallpaper
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
|
||||
class WallpaperRotationWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters) {
|
||||
override fun doWork(): Result {
|
||||
if (!WallpaperStore.enabled(applicationContext)) return Result.success()
|
||||
if (WallpaperStore.applyNext(applicationContext)) return Result.success()
|
||||
return if (runAttemptCount < 2) Result.retry() else Result.failure()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val WORK_NAME = "wallpaper-rotation"
|
||||
|
||||
fun enqueue(context: Context) {
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(
|
||||
WORK_NAME,
|
||||
ExistingWorkPolicy.KEEP,
|
||||
OneTimeWorkRequestBuilder<WallpaperRotationWorker>().build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,8 @@ object WallpaperScheduler {
|
||||
}
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val storedTrigger = preferences(context).getLong(KEY_NEXT_TRIGGER, 0L)
|
||||
if (storedTrigger <= now) scheduleNext(context)
|
||||
val interval = WallpaperStore.intervalMinutes(context) * 60_000L
|
||||
if (storedTrigger <= now || storedTrigger > now + interval) scheduleNext(context)
|
||||
}
|
||||
|
||||
fun remainingDelay(context: Context): Long {
|
||||
@@ -32,10 +33,11 @@ object WallpaperScheduler {
|
||||
val preferences = preferences(context)
|
||||
val trigger = preferences.getLong(KEY_NEXT_TRIGGER, 0L)
|
||||
if (trigger <= 0L || trigger > SystemClock.elapsedRealtime() + 1_000L) return false
|
||||
preferences.edit().putLong(KEY_NEXT_TRIGGER, Long.MAX_VALUE).commit()
|
||||
scheduleNext(context)
|
||||
return true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun scheduleNext(context: Context) {
|
||||
val minutes = WallpaperStore.intervalMinutes(context)
|
||||
if (minutes <= 0) {
|
||||
@@ -50,7 +52,7 @@ object WallpaperScheduler {
|
||||
} else {
|
||||
alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, trigger, operation)
|
||||
}
|
||||
preferences(context).edit().putLong(KEY_NEXT_TRIGGER, trigger).apply()
|
||||
preferences(context).edit().putLong(KEY_NEXT_TRIGGER, trigger).commit()
|
||||
}
|
||||
|
||||
fun cancel(context: Context) {
|
||||
|
||||
@@ -119,6 +119,10 @@ object WallpaperStore {
|
||||
put("shuffle", shuffle(context))
|
||||
put("lockScreenOnly", lockOnly(context))
|
||||
put("currentIndex", previewIndex)
|
||||
put("currentId", items.getOrNull(index)?.id ?: JSONObject.NULL)
|
||||
val ids = JSArray()
|
||||
previewItems.forEach { ids.put(it.id) }
|
||||
put("imageIds", ids)
|
||||
val previews = JSArray()
|
||||
if (includePreviews) previewItems.forEach { previews.put(thumbnailDataUrl(it.preview)) }
|
||||
put("imageUrls", previews)
|
||||
@@ -318,6 +322,18 @@ object WallpaperStore {
|
||||
val candidates = if (shuffle(context) && items.size > 1) {
|
||||
items.indices.filter { it != previous }.shuffled() + previous
|
||||
} else (1..items.size).map { (previous + it).mod(items.size) }
|
||||
return applyCandidates(context, items, candidates)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun apply(context: Context, id: String): Boolean {
|
||||
val items = entries(context)
|
||||
val index = items.indexOfFirst { it.id == id }
|
||||
if (index < 0) return false
|
||||
return applyCandidates(context, items, listOf(index))
|
||||
}
|
||||
|
||||
private fun applyCandidates(context: Context, items: List<Entry>, candidates: List<Int>): Boolean {
|
||||
val unavailableServers = mutableSetOf<String>()
|
||||
for (index in candidates) {
|
||||
val entry = items[index]
|
||||
|
||||
Reference in New Issue
Block a user