feat(android): switch to screen-on rotation service and updated scheduler

The rotation service now responds to screen on/off events via a
dedicated ScreenOnRotationService, reducing background work and
avoiding perpetual wakeful states. Manifest and receiver logic were
adjusted to align with the new behavior and energy constraints.

Scheduling and enablement now consistently use timed-based rules and
screen-on preferences, with the boot and alarm paths updated to honor
these settings. This avoids unnecessary foreground services and keeps
the app compliant with Doze and battery optimizations.

Immich image handling improvements include throttled progress updates,
larger cache allowances, and a connectivity-aware automatic download
check to avoid unwanted network usage.

- Use ScreenOnRotationService for screen-on based rotation scheduling
- Centralize enablement via timed-enabled paths and AlarmManager windowing
- Improve Immich cache and progress reporting with connectivity checks
This commit is contained in:
2026-08-22 20:04:42 +02:00
parent 73f8599c7f
commit 75c79a8344
26 changed files with 245 additions and 161 deletions
+3 -3
View File
@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<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" />
<service android:name="de.wechselbild.wallpaper.ScreenOnRotationService" 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 when the screen turns on" />
</service>
<receiver android:name="de.wechselbild.wallpaper.BootReceiver" android:enabled="true" android:exported="true">
<intent-filter>
@@ -6,9 +6,10 @@ import android.content.Intent
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action in setOf(Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED) && WallpaperStore.enabled(context)) {
WallpaperScheduler.scheduleNext(context)
runCatching { WallpaperRotationService.start(context) }
if (intent.action !in setOf(Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED)) return
when {
WallpaperStore.screenOnEnabled(context) -> runCatching { ScreenOnRotationService.start(context) }
WallpaperStore.timedEnabled(context) -> WallpaperScheduler.scheduleNext(context)
}
}
}
+21 -10
View File
@@ -1,6 +1,7 @@
package de.wechselbild.wallpaper
import android.content.Context
import android.net.ConnectivityManager
import android.graphics.BitmapFactory
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
@@ -33,9 +34,9 @@ object ImmichClient {
private const val KEY_API_KEY_DATA = "immich_api_key_data"
private const val KEY_API_KEY_IV = "immich_api_key_iv"
private const val KEY_ALIAS = "wallpaperflow_immich_api_key"
private const val CACHE_MAX_FILES = 3
private const val CACHE_MAX_BYTES = 256L * 1024 * 1024
private const val CACHE_MAX_BYTES = 512L * 1024 * 1024
private const val DOWNLOAD_MAX_BYTES = 128L * 1024 * 1024
private const val PROGRESS_STEP_BYTES = 256L * 1024
private val assetIdPattern = Regex("^[0-9a-fA-F-]{36}$")
private val thumbnailPool = Executors.newFixedThreadPool(4)
private val importProgress = AtomicReference(ImportProgress())
@@ -204,6 +205,7 @@ object ImmichClient {
val total = connection.contentLengthLong.coerceAtLeast(0)
require(total == 0L || total <= DOWNLOAD_MAX_BYTES) { "Das Immich-Bild ist zu groß." }
var downloaded = 0L
var lastProgressUpdate = 0L
if (trackProgress) importProgress.updateAndGet { it.copy(bytesDownloaded = 0, bytesTotal = total) }
connection.inputStream.use { input ->
target.outputStream().use { output ->
@@ -214,7 +216,10 @@ object ImmichClient {
output.write(buffer, 0, count)
downloaded += count
require(downloaded <= DOWNLOAD_MAX_BYTES) { "Das Immich-Bild ist zu groß." }
if (trackProgress) importProgress.updateAndGet { it.copy(bytesDownloaded = downloaded, bytesTotal = total) }
if (trackProgress && (downloaded - lastProgressUpdate >= PROGRESS_STEP_BYTES || downloaded == total)) {
lastProgressUpdate = downloaded
importProgress.updateAndGet { it.copy(bytesDownloaded = downloaded, bytesTotal = total) }
}
}
}
}
@@ -309,6 +314,12 @@ object ImmichClient {
return bounds.outWidth > 0 && bounds.outHeight > 0
}
fun automaticDownloadsAllowed(context: Context): Boolean {
val connectivity = context.getSystemService(ConnectivityManager::class.java)
@Suppress("DEPRECATION")
return connectivity.activeNetworkInfo?.isConnected == true && !connectivity.isActiveNetworkMetered
}
@Synchronized
fun cachedOriginal(context: Context, serverUrl: String, assetId: String, allowNetwork: Boolean = true): File? {
val target = File(cacheDirectory(context), "${cacheKey(serverUrl, assetId)}.image")
@@ -347,13 +358,13 @@ object ImmichClient {
}
private fun trimCache(context: Context, protected: File) {
val files = cacheDirectory(context).listFiles()?.filter { it.isFile && !it.name.startsWith(".") }?.sortedByDescending { it.lastModified() } ?: return
var bytes = 0L
files.forEachIndexed { index, file ->
bytes += file.length()
if (file != protected && (index >= CACHE_MAX_FILES || bytes > CACHE_MAX_BYTES)) {
bytes -= file.length()
file.delete()
val files = cacheDirectory(context).listFiles()?.filter { it.isFile && !it.name.startsWith(".") }?.sortedBy { it.lastModified() } ?: return
var bytes = files.sumOf { it.length() }
for (file in files) {
if (bytes <= CACHE_MAX_BYTES) break
if (file != protected) {
val length = file.length()
if (file.delete()) bytes -= length
}
}
}
@@ -4,37 +4,46 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
class WallpaperRotationService : Service() {
class ScreenOnRotationService : Service() {
private val worker = Executors.newSingleThreadExecutor()
private val handler = Handler(Looper.getMainLooper())
private val rotate = object : Runnable {
override fun run() {
if (!WallpaperStore.enabled(this@WallpaperRotationService)) {
stopSelf()
return
}
worker.execute {
if (WallpaperScheduler.claimIfDue(this@WallpaperRotationService)) {
WallpaperRotationWorker.enqueue(this@WallpaperRotationService)
private val rotating = AtomicBoolean(false)
private val changedWhileScreenOff = AtomicBoolean(false)
private val screenReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (!WallpaperStore.screenOnEnabled(context)) return
when (intent.action) {
Intent.ACTION_SCREEN_OFF -> rotate(context, preparedForScreenOn = true)
Intent.ACTION_SCREEN_ON -> {
if (!changedWhileScreenOff.getAndSet(false)) rotate(context, preparedForScreenOn = false)
}
handler.post { scheduleLocalTimer() }
}
}
}
private fun rotate(context: Context, preparedForScreenOn: Boolean) {
if (!rotating.compareAndSet(false, true)) return
worker.execute {
try {
if (WallpaperStore.applyNext(context, automatic = true) && preparedForScreenOn) {
changedWhileScreenOff.set(true)
}
} finally { rotating.set(false) }
}
}
override fun onCreate() {
super.onCreate()
runningService = this
createChannel()
val launch = packageManager.getLaunchIntentForPackage(packageName)
val pending = PendingIntent.getActivity(this, 0, launch, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
@@ -42,34 +51,49 @@ class WallpaperRotationService : Service() {
.setSmallIcon(android.R.drawable.ic_menu_gallery)
.setContentTitle(getString(R.string.wallpaper_service_title))
.setContentText(getString(R.string.wallpaper_service_text))
.setOngoing(true).setSilent(true).setContentIntent(pending).build()
.setOngoing(true)
.setSilent(true)
.setContentIntent(pending)
.build()
startForeground(NOTIFICATION_ID, notification)
WallpaperScheduler.ensureScheduled(this)
scheduleLocalTimer()
val filter = IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_SCREEN_ON)
}
if (Build.VERSION.SDK_INT >= 33) {
registerReceiver(screenReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
@Suppress("DEPRECATION")
registerReceiver(screenReceiver, filter)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (!WallpaperStore.enabled(this)) {
if (!WallpaperStore.screenOnEnabled(this)) {
stopSelf()
return START_NOT_STICKY
}
WallpaperScheduler.ensureScheduled(this)
scheduleLocalTimer()
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
handler.removeCallbacks(rotate)
runCatching { unregisterReceiver(screenReceiver) }
worker.shutdown()
if (runningService === this) runningService = null
super.onDestroy()
}
private fun scheduleLocalTimer() {
handler.removeCallbacks(rotate)
if (WallpaperStore.enabled(this)) {
handler.postDelayed(rotate, WallpaperScheduler.remainingDelay(this))
companion object {
private const val CHANNEL = "wechselbild_screen_on"
private const val NOTIFICATION_ID = 4217
fun start(context: Context) {
ContextCompat.startForegroundService(context, Intent(context, ScreenOnRotationService::class.java))
}
fun stop(context: Context) {
context.stopService(Intent(context, ScreenOnRotationService::class.java))
}
}
@@ -79,27 +103,4 @@ class WallpaperRotationService : Service() {
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}
}
companion object {
private const val CHANNEL = "wechselbild_rotation"
private const val NOTIFICATION_ID = 4217
@Volatile private var runningService: WallpaperRotationService? = null
fun rescheduleRunningService() {
val service = runningService ?: return
service.handler.post { service.scheduleLocalTimer() }
}
fun start(context: Context) {
WallpaperScheduler.ensureScheduled(context)
ContextCompat.startForegroundService(context, Intent(context, WallpaperRotationService::class.java))
}
fun restart(context: Context) {
WallpaperScheduler.scheduleNext(context)
start(context)
}
fun stop(context: Context) {
WallpaperScheduler.cancel(context)
context.stopService(Intent(context, WallpaperRotationService::class.java))
}
}
}
@@ -6,7 +6,7 @@ import android.content.Intent
class WallpaperAlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (!WallpaperStore.enabled(context)) {
if (!WallpaperStore.timedEnabled(context)) {
WallpaperScheduler.cancel(context)
return
}
@@ -14,6 +14,5 @@ class WallpaperAlarmReceiver : BroadcastReceiver() {
WallpaperRotationWorker.enqueue(context)
}
WallpaperScheduler.ensureScheduled(context)
WallpaperRotationService.rescheduleRunningService()
}
}
@@ -58,7 +58,6 @@ class WallpaperPlugin(private val activity: Activity) : Plugin(activity) {
private val io = Executors.newSingleThreadExecutor()
@Command fun getState(invoke: Invoke) {
if (WallpaperStore.enabled(activity)) runCatching { WallpaperRotationService.start(activity) }
io.execute { invoke.resolve(WallpaperStore.state(activity)) }
}
@@ -141,7 +140,10 @@ class WallpaperPlugin(private val activity: Activity) : Plugin(activity) {
try {
val args = invoke.parseArgs(IntervalArgs::class.java)
WallpaperStore.setInterval(activity, args.minutes)
if (args.minutes > 0) WallpaperRotationService.restart(activity) else WallpaperRotationService.stop(activity)
WallpaperScheduler.cancel(activity)
ScreenOnRotationService.stop(activity)
if (args.minutes == -1) ScreenOnRotationService.start(activity)
else if (args.minutes > 0) WallpaperScheduler.scheduleNext(activity)
invoke.resolve(WallpaperStore.state(activity))
} catch (error: Exception) { invoke.reject(error.message ?: "Wechselintervall konnte nicht gespeichert werden") }
}
@@ -2,6 +2,7 @@ package de.wechselbild.wallpaper
import android.content.Context
import androidx.work.ExistingWorkPolicy
import androidx.work.Constraints
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
@@ -9,9 +10,9 @@ 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()
if (!WallpaperStore.timedEnabled(applicationContext)) return Result.success()
WallpaperStore.applyNext(applicationContext, automatic = true)
return Result.success()
}
companion object {
@@ -21,7 +22,9 @@ class WallpaperRotationWorker(context: Context, parameters: WorkerParameters) :
WorkManager.getInstance(context).enqueueUniqueWork(
WORK_NAME,
ExistingWorkPolicy.KEEP,
OneTimeWorkRequestBuilder<WallpaperRotationWorker>().build(),
OneTimeWorkRequestBuilder<WallpaperRotationWorker>()
.setConstraints(Constraints.Builder().setRequiresBatteryNotLow(true).build())
.build(),
)
}
}
@@ -4,7 +4,6 @@ import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.SystemClock
object WallpaperScheduler {
@@ -13,7 +12,7 @@ object WallpaperScheduler {
private const val REQUEST_CODE = 4218
fun ensureScheduled(context: Context) {
if (!WallpaperStore.enabled(context)) {
if (!WallpaperStore.timedEnabled(context)) {
cancel(context)
return
}
@@ -23,11 +22,6 @@ object WallpaperScheduler {
if (storedTrigger <= now || storedTrigger > now + interval) scheduleNext(context)
}
fun remainingDelay(context: Context): Long {
val trigger = preferences(context).getLong(KEY_NEXT_TRIGGER, 0L)
return (trigger - SystemClock.elapsedRealtime()).coerceAtLeast(0L)
}
@Synchronized
fun claimIfDue(context: Context): Boolean {
val preferences = preferences(context)
@@ -47,11 +41,8 @@ object WallpaperScheduler {
val trigger = SystemClock.elapsedRealtime() + minutes * 60_000L
val alarmManager = context.getSystemService(AlarmManager::class.java)
val operation = operation(context)
if (Build.VERSION.SDK_INT >= 23) {
alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, trigger, operation)
} else {
alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, trigger, operation)
}
val window = (minutes * 60_000L / 10).coerceIn(60_000L, 15 * 60_000L)
alarmManager.setWindow(AlarmManager.ELAPSED_REALTIME, trigger, window, operation)
preferences(context).edit().putLong(KEY_NEXT_TRIGGER, trigger).commit()
}
+100 -26
View File
@@ -23,6 +23,9 @@ object WallpaperStore {
private const val KEY_CURRENT_ID = "current_entry_id"
private const val KEY_INTERVAL = "interval_minutes"
private const val CROP_PREFIX = "crop_"
private const val INVALID_PREFIX = "invalid_"
private const val RENDER_CACHE_MAX_BYTES = 128L * 1024 * 1024
private const val THUMBNAIL_CACHE_MAX_BYTES = 64L * 1024 * 1024
private const val HOME_PREVIEW_LIMIT = 12
private val thumbnailCache = LruCache<String, String>(48)
@@ -47,6 +50,8 @@ object WallpaperStore {
private fun immichEntries(context: Context) = File(immichRoot(context), "entries").apply { mkdirs() }
private fun immichPreviews(context: Context) = File(immichRoot(context), "previews").apply { mkdirs() }
private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
private fun renderedDirectory(context: Context) = File(context.cacheDir, "rendered-wallpapers").apply { mkdirs() }
private fun thumbnailDirectory(context: Context) = File(context.cacheDir, "wallpaper-thumbnails").apply { mkdirs() }
private fun serverKey(serverUrl: String): String = MessageDigest.getInstance("SHA-256")
.digest(serverUrl.toByteArray(Charsets.UTF_8)).take(6).joinToString("") { "%02x".format(it) }
@@ -92,7 +97,9 @@ object WallpaperStore {
return if (preferences.contains(KEY_INTERVAL)) preferences.getInt(KEY_INTERVAL, 0)
else if (preferences.getBoolean("enabled", false)) 30 else 0
}
fun enabled(context: Context) = intervalMinutes(context) > 0
fun enabled(context: Context) = intervalMinutes(context) != 0
fun timedEnabled(context: Context) = intervalMinutes(context) > 0
fun screenOnEnabled(context: Context) = intervalMinutes(context) == -1
fun shuffle(context: Context) = prefs(context).getBoolean("shuffle", true)
fun lockOnly(context: Context) = prefs(context).getBoolean("lockScreenOnly", true)
@@ -102,8 +109,8 @@ object WallpaperStore {
}
fun setInterval(context: Context, minutes: Int) {
require(minutes == 0 || minutes in 5..720) { "Ungültiges Wechselintervall" }
prefs(context).edit().putInt(KEY_INTERVAL, minutes).putBoolean("enabled", minutes > 0).apply()
require(minutes == -1 || minutes == 0 || minutes in 5..720) { "Ungültiges Wechselintervall" }
prefs(context).edit().putInt(KEY_INTERVAL, minutes).putBoolean("enabled", minutes != 0).commit()
}
@Synchronized
@@ -124,7 +131,7 @@ object WallpaperStore {
previewItems.forEach { ids.put(it.id) }
put("imageIds", ids)
val previews = JSArray()
if (includePreviews) previewItems.forEach { previews.put(thumbnailDataUrl(it.preview)) }
if (includePreviews) previewItems.forEach { previews.put(thumbnailDataUrl(context, it.preview)) }
put("imageUrls", previews)
}
}
@@ -164,7 +171,7 @@ object WallpaperStore {
val crop = crop(context, entry)
return JSObject().apply {
put("id", entry.id)
put("url", thumbnailDataUrl(entry.preview))
put("url", thumbnailDataUrl(context, entry.preview))
put("selected", selected)
put("cropMode", crop.mode)
put("cropZoom", crop.zoom)
@@ -294,9 +301,16 @@ object WallpaperStore {
return BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options().apply { inSampleSize = sample; inPreferredConfig = Bitmap.Config.ARGB_8888 })
}
private fun thumbnailDataUrl(file: File): String {
private fun thumbnailDataUrl(context: Context, file: File): String {
val cacheKey = "${file.absolutePath}:${file.lastModified()}:${file.length()}"
thumbnailCache.get(cacheKey)?.let { return it }
val cached = File(thumbnailDirectory(context), "${digest(cacheKey)}.jpg")
if (cached.isFile) {
cached.setLastModified(System.currentTimeMillis())
val result = "data:image/jpeg;base64," + Base64.encodeToString(cached.readBytes(), Base64.NO_WRAP)
thumbnailCache.put(cacheKey, result)
return result
}
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(file.absolutePath, bounds)
var sample = 1
@@ -304,7 +318,9 @@ object WallpaperStore {
val bitmap = BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options().apply { inSampleSize = sample }) ?: return ""
val result = ByteArrayOutputStream().use { out ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 76, out); bitmap.recycle()
"data:image/jpeg;base64," + Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP)
val bytes = out.toByteArray()
runCatching { cached.writeBytes(bytes); trimDirectory(thumbnailDirectory(context), THUMBNAIL_CACHE_MAX_BYTES, cached) }
"data:image/jpeg;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP)
}
thumbnailCache.put(cacheKey, result)
return result
@@ -314,15 +330,52 @@ object WallpaperStore {
thumbnailCache.remove("${file.absolutePath}:${file.lastModified()}:${file.length()}")
}
private fun digest(value: String): String = MessageDigest.getInstance("SHA-256")
.digest(value.toByteArray(Charsets.UTF_8)).joinToString("") { "%02x".format(it) }
private fun renderedFile(context: Context, entry: Entry): File {
val metrics = context.resources.displayMetrics
val sourceVersion = when (entry) {
is Entry.Local -> "${entry.file.lastModified()}:${entry.file.length()}"
is Entry.Immich -> entry.assetId
}
val key = "${entry.id}:$sourceVersion:${metrics.widthPixels}x${metrics.heightPixels}:${prefs(context).getString(cropKey(entry.id), "")}"
return File(renderedDirectory(context), "${digest(key)}.jpg")
}
private fun sourceVersion(entry: Entry): String = when (entry) {
is Entry.Local -> "${entry.file.lastModified()}:${entry.file.length()}"
is Entry.Immich -> entry.assetId
}
private fun validImage(file: File): Boolean {
if (!file.isFile || file.length() <= 0) return false
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(file.absolutePath, bounds)
return bounds.outWidth > 0 && bounds.outHeight > 0
}
private fun trimDirectory(directory: File, maxBytes: Long, protected: File) {
val files = directory.listFiles()?.filter { it.isFile }?.sortedBy { it.lastModified() } ?: return
var bytes = files.sumOf { it.length() }
for (file in files) {
if (bytes <= maxBytes) break
if (file != protected) {
val length = file.length()
if (file.delete()) bytes -= length
}
}
}
@Synchronized
fun applyNext(context: Context): Boolean {
fun applyNext(context: Context, automatic: Boolean = false): Boolean {
val items = entries(context)
if (items.isEmpty()) return false
val previous = currentIndex(context, items)
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)
return applyCandidates(context, items, candidates, automatic)
}
@Synchronized
@@ -330,37 +383,58 @@ object WallpaperStore {
val items = entries(context)
val index = items.indexOfFirst { it.id == id }
if (index < 0) return false
return applyCandidates(context, items, listOf(index))
return applyCandidates(context, items, listOf(index), automatic = false)
}
private fun applyCandidates(context: Context, items: List<Entry>, candidates: List<Int>): Boolean {
private fun applyCandidates(context: Context, items: List<Entry>, candidates: List<Int>, automatic: Boolean): Boolean {
val unavailableServers = mutableSetOf<String>()
for (index in candidates) {
val entry = items[index]
val sourceFile = when (entry) {
is Entry.Local -> entry.file
is Entry.Immich -> {
val source = ImmichClient.cachedOriginal(context, entry.serverUrl, entry.assetId, entry.serverUrl !in unavailableServers)
if (source == null) {
unavailableServers.add(entry.serverUrl)
continue
val renderedFile = renderedFile(context, entry)
var rendered = if (validImage(renderedFile)) {
renderedFile.setLastModified(System.currentTimeMillis())
BitmapFactory.decodeFile(renderedFile.absolutePath)
} else null
if (rendered == null) {
renderedFile.delete()
if (prefs(context).getString(INVALID_PREFIX + entry.id, null) == sourceVersion(entry)) continue
val sourceFile = when (entry) {
is Entry.Local -> entry.file
is Entry.Immich -> {
val allowNetwork = entry.serverUrl !in unavailableServers && (!automatic || ImmichClient.automaticDownloadsAllowed(context))
val source = ImmichClient.cachedOriginal(context, entry.serverUrl, entry.assetId, allowNetwork)
if (source == null) {
if (allowNetwork) unavailableServers.add(entry.serverUrl)
continue
}
source
}
source
}
val bitmap = decodeForScreen(context, sourceFile)
if (bitmap == null) {
prefs(context).edit().putString(INVALID_PREFIX + entry.id, sourceVersion(entry)).apply()
continue
}
val generated = runCatching { renderForScreen(context, bitmap, crop(context, entry)) }.getOrNull()
bitmap.recycle()
if (generated == null) continue
runCatching {
renderedFile.outputStream().use { generated.compress(Bitmap.CompressFormat.JPEG, 92, it) }
trimDirectory(renderedDirectory(context), RENDER_CACHE_MAX_BYTES, renderedFile)
}
rendered = generated
}
val bitmap = decodeForScreen(context, sourceFile) ?: continue
val rendered = runCatching { renderForScreen(context, bitmap, crop(context, entry)) }.getOrNull()
if (rendered == null) { bitmap.recycle(); continue }
val wallpaper = rendered ?: continue
try {
val manager = WallpaperManager.getInstance(context)
if (android.os.Build.VERSION.SDK_INT >= 24 && lockOnly(context)) manager.setBitmap(rendered, null, true, WallpaperManager.FLAG_LOCK)
else manager.setBitmap(rendered)
prefs(context).edit().putString(KEY_CURRENT_ID, entry.id).putInt(KEY_INDEX, index).apply()
if (android.os.Build.VERSION.SDK_INT >= 24 && lockOnly(context)) manager.setBitmap(wallpaper, null, true, WallpaperManager.FLAG_LOCK)
else manager.setBitmap(wallpaper)
prefs(context).edit().remove(INVALID_PREFIX + entry.id).putString(KEY_CURRENT_ID, entry.id).putInt(KEY_INDEX, index).apply()
return true
} catch (_: Exception) {
// Try the next usable entry without changing the current selection.
} finally {
rendered.recycle(); bitmap.recycle()
wallpaper.recycle()
}
}
return false
@@ -1,5 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow ist aktiv</string>
<string name="wallpaper_service_text">Das Motiv wechselt nach dem eingestellten Intervall.</string>
<string name="wallpaper_channel_name">Automatischer Bildwechsel</string>
<string name="wallpaper_service_text">Das nächste Motiv wird beim Ausschalten des Displays vorbereitet.</string>
<string name="wallpaper_channel_name">Bildwechsel bei Display-Aktivierung</string>
</resources>
@@ -1,5 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow está activo</string>
<string name="wallpaper_service_text">El fondo cambia según el intervalo configurado.</string>
<string name="wallpaper_channel_name">Rotación automática de fondos</string>
<string name="wallpaper_service_text">El fondo cambia cuando se enciende la pantalla.</string>
<string name="wallpaper_channel_name">Rotación al encender la pantalla</string>
</resources>
@@ -1,5 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow est actif</string>
<string name="wallpaper_service_text">Le fond d’écran change selon lintervalle défini.</string>
<string name="wallpaper_channel_name">Rotation automatique des fonds d’écran</string>
<string name="wallpaper_service_text">Le fond d’écran change lorsque l’écran sallume.</string>
<string name="wallpaper_channel_name">Rotation à lallumage de l’écran</string>
</resources>
@@ -1,5 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow è attivo</string>
<string name="wallpaper_service_text">Lo sfondo cambia allintervallo configurato.</string>
<string name="wallpaper_channel_name">Rotazione automatica degli sfondi</string>
<string name="wallpaper_service_text">Lo sfondo cambia quando si accende lo schermo.</string>
<string name="wallpaper_channel_name">Rotazione allaccensione dello schermo</string>
</resources>
@@ -1,5 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow は実行中です</string>
<string name="wallpaper_service_text">設定した間隔で壁紙が切り替わります。</string>
<string name="wallpaper_channel_name">壁紙の自動切り替え</string>
<string name="wallpaper_service_text">画面が点灯すると壁紙が切り替わります。</string>
<string name="wallpaper_channel_name">画面点灯時の壁紙切り替え</string>
</resources>
@@ -1,5 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow 실행 중</string>
<string name="wallpaper_service_text">설정한 간격에 따라 배경화면이 변경됩니다.</string>
<string name="wallpaper_channel_name">자동 배경화면 변경</string>
<string name="wallpaper_service_text">화면이 켜질 때 배경화면이 변경됩니다.</string>
<string name="wallpaper_channel_name">화면 켜짐 시 배경화면 변경</string>
</resources>
@@ -1,5 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow is actief</string>
<string name="wallpaper_service_text">De achtergrond wisselt volgens het ingestelde interval.</string>
<string name="wallpaper_channel_name">Automatisch achtergronden wisselen</string>
<string name="wallpaper_service_text">De achtergrond wisselt wanneer het scherm aangaat.</string>
<string name="wallpaper_channel_name">Wisselen bij scherminschakeling</string>
</resources>
@@ -1,5 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow jest aktywny</string>
<string name="wallpaper_service_text">Tapeta zmienia się zgodnie z ustawionym interwałem.</string>
<string name="wallpaper_channel_name">Automatyczna zmiana tapety</string>
<string name="wallpaper_service_text">Tapeta zmienia się po włączeniu ekranu.</string>
<string name="wallpaper_channel_name">Zmiana po włączeniu ekranu</string>
</resources>
@@ -1,5 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow está ativo</string>
<string name="wallpaper_service_text">O fundo muda de acordo com o intervalo definido.</string>
<string name="wallpaper_channel_name">Rotação automática de fundos</string>
<string name="wallpaper_service_text">O fundo muda quando o ecrã é ligado.</string>
<string name="wallpaper_channel_name">Rotação ao ligar o ecrã</string>
</resources>
@@ -1,5 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow 正在运行</string>
<string name="wallpaper_service_text">壁纸会按照设定的间隔更换</string>
<string name="wallpaper_channel_name">自动更换壁纸</string>
<string name="wallpaper_service_text">屏幕亮起时会更换壁纸。</string>
<string name="wallpaper_channel_name">亮屏时更换壁纸</string>
</resources>
@@ -1,5 +1,5 @@
<resources>
<string name="wallpaper_service_title">WallpaperFlow is active</string>
<string name="wallpaper_service_text">The wallpaper changes at the configured interval.</string>
<string name="wallpaper_channel_name">Automatic wallpaper rotation</string>
<string name="wallpaper_service_text">The next wallpaper is prepared when the screen turns off.</string>
<string name="wallpaper_channel_name">Screen-on wallpaper rotation</string>
</resources>
+1 -1
View File
@@ -64,7 +64,7 @@ pub(crate) async fn set_setting<R: Runtime>(
app.wallpaper().set_setting(SettingRequest { name, value })
}
#[command]
pub(crate) async fn set_interval<R: Runtime>(app: AppHandle<R>, minutes: usize) -> Result<WallpaperState> {
pub(crate) async fn set_interval<R: Runtime>(app: AppHandle<R>, minutes: i32) -> Result<WallpaperState> {
app.wallpaper().set_interval(IntervalRequest { minutes })
}
#[command]
+1 -1
View File
@@ -96,7 +96,7 @@ impl<R: Runtime> Wallpaper<R> {
pub fn set_interval(&self, payload: IntervalRequest) -> crate::Result<WallpaperState> {
let mut state = Self::demo();
state.interval_minutes = payload.minutes;
state.enabled = payload.minutes > 0;
state.enabled = payload.minutes != 0;
Ok(state)
}
pub fn next_wallpaper(&self) -> crate::Result<WallpaperState> {
+2 -2
View File
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
pub struct WallpaperState {
pub image_count: usize,
pub enabled: bool,
pub interval_minutes: usize,
pub interval_minutes: i32,
pub shuffle: bool,
pub lock_screen_only: bool,
pub current_index: usize,
@@ -24,7 +24,7 @@ pub struct SettingRequest {
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IntervalRequest {
pub minutes: usize,
pub minutes: i32,
}
#[derive(Debug, Clone, Deserialize, Serialize)]