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 {