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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package com.sameerasw.essentials.data.repository

import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.sameerasw.essentials.domain.model.WidgetStackConfig
Expand Down Expand Up @@ -43,6 +44,32 @@ class WidgetStackRepository(

fun get(stackWidgetId: Int): WidgetStackConfig? = getAll().firstOrNull { it.stackWidgetId == stackWidgetId }

fun getPositions(): Map<Int, Int> {
val result = mutableMapOf<Int, Int>()
for ((key, value) in prefs.all) {
if (key.startsWith(KEY_POSITION_PREFIX) && value is Int) {
val id = key.removePrefix(KEY_POSITION_PREFIX).toIntOrNull() ?: continue
result[id] = value
}
}
return result
}

fun savePositions(positions: Map<Int, Int>) {
val toSave =
positions.filter { (id, pos) ->
val key = KEY_POSITION_PREFIX + id
val stored = if (prefs.contains(key)) prefs.getInt(key, 0) else null
stored != pos
}
if (toSave.isEmpty()) return
prefs.edit {
for ((id, pos) in toSave) {
putInt(KEY_POSITION_PREFIX + id, pos)
}
}
}

fun save(config: WidgetStackConfig) =
synchronized(LOCK) {
write(getAll().filterNot { it.stackWidgetId == config.stackWidgetId } + config)
Expand All @@ -51,6 +78,7 @@ class WidgetStackRepository(
fun delete(stackWidgetId: Int) =
synchronized(LOCK) {
write(getAll().filterNot { it.stackWidgetId == stackWidgetId })
prefs.edit { remove(KEY_POSITION_PREFIX + stackWidgetId) }
}

private fun write(stacks: List<WidgetStackConfig>) {
Expand All @@ -63,6 +91,7 @@ class WidgetStackRepository(
companion object {
private const val PREFS_NAME = "widget_stack_prefs"
private const val KEY_STACKS = "stacks"
private const val KEY_POSITION_PREFIX = "position_"
private val LOCK = Any()
private var cachedJson: String? = null
private var cachedStacks: List<WidgetStackConfig> = emptyList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import android.appwidget.AppWidgetHostView
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.BroadcastReceiver
import android.content.ComponentCallbacks
import android.content.ComponentCallbacks2
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
Expand Down Expand Up @@ -102,20 +102,28 @@ object StackHost {
intent: Intent,
) {
when (intent.action) {
Intent.ACTION_SCREEN_OFF -> cancelAllTimers()
Intent.ACTION_SCREEN_OFF -> {
cancelAllTimers()
repository.savePositions(positions)
}

Intent.ACTION_SCREEN_ON -> repository.getAll().forEach { scheduleTimer(it.stackWidgetId) }
}
}
}

private val configurationCallbacks =
object : ComponentCallbacks {
object : ComponentCallbacks2 {
override fun onConfigurationChanged(newConfig: Configuration) {
repository.getAll().forEach { StackWidgetProvider.forwardSize(appContext, it) }
}

@Deprecated("Deprecated in Java")
override fun onLowMemory() {}

override fun onTrimMemory(level: Int) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) repository.savePositions(positions)
}
}

fun startIfNeeded(context: Context) {
Expand All @@ -140,6 +148,9 @@ object StackHost {
if (host == null) start(app)
reveal(stackWidgetId)
advance(stackWidgetId, delta)
positions[stackWidgetId]?.let { pos ->
repository.savePositions(mapOf(stackWidgetId to pos))
}
}
}

Expand Down Expand Up @@ -177,6 +188,7 @@ object StackHost {
PeriodicWorkRequestBuilder<StackHostWorker>(15, TimeUnit.MINUTES).build(),
)
repository = WidgetStackRepository(context)
positions.putAll(StackPositions.restore(repository.getPositions(), repository.getAll()))
host = CapturingWidgetHost(context).also { it.startListening() }
removeOrphanedWidgets()
attachOverlay()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2026 sameerasw.com
* License: MIT License
*
* Feature Module: Background Services & Receivers
* File: StackPositions.kt
* Description: Restores and validates widget stack positions.
*/

package com.sameerasw.essentials.services.widgets

import com.sameerasw.essentials.domain.model.WidgetStackConfig

object StackPositions {
fun restore(
saved: Map<Int, Int>,
stacks: List<WidgetStackConfig>,
): Map<Int, Int> =
buildMap {
for (stack in stacks) {
if (stack.hostedWidgetIds.isEmpty()) continue
val pos = saved[stack.stackWidgetId] ?: continue
put(stack.stackWidgetId, pos.coerceIn(0, stack.hostedWidgetIds.lastIndex))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2026 sameerasw.com
* License: MIT License
*
* Feature Module: Unit Tests - Services
* File: StackPositionsTest.kt
* Description: Unit tests for StackPositions restore logic.
*/

package com.sameerasw.essentials.services

import com.sameerasw.essentials.domain.model.WidgetStackConfig
import com.sameerasw.essentials.services.widgets.StackPositions
import org.junit.Assert.assertEquals
import org.junit.Test

class StackPositionsTest {
@Test
fun keepsSavedPositionInRange() {
val stacks = listOf(WidgetStackConfig(stackWidgetId = 1, hostedWidgetIds = listOf(10, 11, 12)))
val saved = mapOf(1 to 1)
assertEquals(mapOf(1 to 1), StackPositions.restore(saved, stacks))
}

@Test
fun clampsSavedPositionAboveLastIndexToLastIndex() {
val stacks = listOf(WidgetStackConfig(stackWidgetId = 1, hostedWidgetIds = listOf(10, 11, 12)))
val saved = mapOf(1 to 5)
assertEquals(mapOf(1 to 2), StackPositions.restore(saved, stacks))
}

@Test
fun clampsNegativeSavedPositionToZero() {
val stacks = listOf(WidgetStackConfig(stackWidgetId = 1, hostedWidgetIds = listOf(10, 11, 12)))
val saved = mapOf(1 to -3)
assertEquals(mapOf(1 to 0), StackPositions.restore(saved, stacks))
}

@Test
fun dropsPositionsForStackIdsNotInStacks() {
val stacks = listOf(WidgetStackConfig(stackWidgetId = 1, hostedWidgetIds = listOf(10, 11)))
val saved = mapOf(1 to 1, 2 to 0, 99 to 3)
assertEquals(mapOf(1 to 1), StackPositions.restore(saved, stacks))
}

@Test
fun leavesOutStacksWithNoHostedWidgets() {
val stacks =
listOf(
WidgetStackConfig(stackWidgetId = 1, hostedWidgetIds = emptyList()),
WidgetStackConfig(stackWidgetId = 2, hostedWidgetIds = listOf(20)),
)
val saved = mapOf(1 to 0, 2 to 0)
assertEquals(mapOf(2 to 0), StackPositions.restore(saved, stacks))
}

@Test
fun leavesOutStacksWithNoSavedPosition() {
val stacks =
listOf(
WidgetStackConfig(stackWidgetId = 1, hostedWidgetIds = listOf(10, 11)),
WidgetStackConfig(stackWidgetId = 2, hostedWidgetIds = listOf(20, 21)),
)
val saved = mapOf(1 to 1)
assertEquals(mapOf(1 to 1), StackPositions.restore(saved, stacks))
}
}
Loading