diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e689234..b31b298 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,12 +7,6 @@
-
-
diff --git a/app/src/main/java/com/omixlab/lckcontrol/data/local/TokenStore.kt b/app/src/main/java/com/omixlab/lckcontrol/data/local/TokenStore.kt
index c866de9..3e16f68 100644
--- a/app/src/main/java/com/omixlab/lckcontrol/data/local/TokenStore.kt
+++ b/app/src/main/java/com/omixlab/lckcontrol/data/local/TokenStore.kt
@@ -1,6 +1,8 @@
package com.omixlab.lckcontrol.data.local
import android.content.Context
+import android.content.SharedPreferences
+import android.util.Log
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -13,13 +15,22 @@ class TokenStore @Inject constructor(
) {
private val masterKey = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
- private val prefs = EncryptedSharedPreferences.create(
- "lck_control_tokens",
- masterKey,
- context,
- EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
- EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
- )
+ private val prefs: SharedPreferences = try {
+ createEncryptedPrefs(context)
+ } catch (e: Exception) {
+ Log.w("TokenStore", "Corrupted keyset, clearing and recreating", e)
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit().clear().commit()
+ createEncryptedPrefs(context)
+ }
+
+ private fun createEncryptedPrefs(context: Context): SharedPreferences =
+ EncryptedSharedPreferences.create(
+ PREFS_NAME,
+ masterKey,
+ context,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
+ )
fun getJwt(): String? =
prefs.getString(KEY_JWT, null)
@@ -44,6 +55,7 @@ class TokenStore @Inject constructor(
fun isLoggedIn(): Boolean = getJwt() != null
companion object {
+ private const val PREFS_NAME = "lck_control_tokens"
private const val KEY_JWT = "session_jwt"
private const val KEY_REFRESH_TOKEN = "session_refresh_token"
}
diff --git a/app/src/main/java/com/omixlab/lckcontrol/service/ClientTracker.kt b/app/src/main/java/com/omixlab/lckcontrol/service/ClientTracker.kt
index ecc56b6..b0f904e 100644
--- a/app/src/main/java/com/omixlab/lckcontrol/service/ClientTracker.kt
+++ b/app/src/main/java/com/omixlab/lckcontrol/service/ClientTracker.kt
@@ -7,6 +7,7 @@ data class ConnectedClient(
val clientId: String,
val clientName: String,
val packageName: String,
+ val callingUid: Int = 0,
val activePlanId: String? = null,
val connectedAt: Long = System.currentTimeMillis(),
)
@@ -15,18 +16,33 @@ class ClientTracker {
private val clients = ConcurrentHashMap()
- fun register(clientName: String, packageName: String): String {
+ fun register(clientName: String, packageName: String, callingUid: Int): String {
+ // Remove existing client with same packageName to prevent duplicates on reconnect
+ clients.entries.removeIf { it.value.packageName == packageName }
+
val clientId = UUID.randomUUID().toString()
clients[clientId] = ConnectedClient(
clientId = clientId,
clientName = clientName,
packageName = packageName,
+ callingUid = callingUid,
)
return clientId
}
fun unregister(clientId: String): ConnectedClient? = clients.remove(clientId)
+ fun unregisterByUid(uid: Int): List {
+ val removed = mutableListOf()
+ clients.entries.removeIf { entry ->
+ if (entry.value.callingUid == uid) {
+ removed.add(entry.value)
+ true
+ } else false
+ }
+ return removed
+ }
+
fun setActivePlan(clientId: String, planId: String?) {
clients.computeIfPresent(clientId) { _, client ->
client.copy(activePlanId = planId)
diff --git a/app/src/main/java/com/omixlab/lckcontrol/service/LckControlService.kt b/app/src/main/java/com/omixlab/lckcontrol/service/LckControlService.kt
index 341f5ac..825a921 100644
--- a/app/src/main/java/com/omixlab/lckcontrol/service/LckControlService.kt
+++ b/app/src/main/java/com/omixlab/lckcontrol/service/LckControlService.kt
@@ -57,7 +57,18 @@ class LckControlService : Service() {
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val clientTracker = ClientTracker()
- private val callbacks = RemoteCallbackList()
+ private val callbacks = object : RemoteCallbackList() {
+ override fun onCallbackDied(callback: ILckControlCallback, cookie: Any?) {
+ val uid = cookie as? Int ?: return
+ serviceScope.launch {
+ val removed = clientTracker.unregisterByUid(uid)
+ for (client in removed) {
+ Log.d(TAG, "Auto-unregistered client ${client.clientId} (${client.packageName}) - process died")
+ broadcastClientUnregistered(client.clientId)
+ }
+ }
+ }
+ }
private val binder = object : ILckControlService.Stub() {
@@ -146,7 +157,8 @@ class LckControlService : Service() {
// ── Clients ─────────────────────────────────────────
override fun registerClient(clientName: String, packageName: String): String {
- val clientId = clientTracker.register(clientName, packageName)
+ val uid = android.os.Binder.getCallingUid()
+ val clientId = clientTracker.register(clientName, packageName, uid)
broadcastClientRegistered(clientId)
return clientId
}
@@ -174,7 +186,8 @@ class LckControlService : Service() {
// ── Callbacks ───────────────────────────────────────
override fun registerCallback(callback: ILckControlCallback) {
- callbacks.register(callback)
+ val uid = android.os.Binder.getCallingUid()
+ callbacks.register(callback, uid)
}
override fun unregisterCallback(callback: ILckControlCallback) {
diff --git a/sdk/build.gradle.kts b/sdk/build.gradle.kts
index b0722d6..60d4556 100644
--- a/sdk/build.gradle.kts
+++ b/sdk/build.gradle.kts
@@ -7,7 +7,7 @@ android {
compileSdk = 36
defaultConfig {
- minSdk = 33
+ minSdk = 32
}
compileOptions {