Remove custom permission, fix client lifecycle and encrypted prefs

Remove USE_LCK_CONTROL permission from service to fix binding failures
caused by Android revoking custom permissions on app reinstall. Add
auto-cleanup of dead clients via RemoteCallbackList.onCallbackDied and
handle corrupted EncryptedSharedPreferences keyset gracefully. Lower
SDK minSdk to 32.
This commit is contained in:
2026-02-27 22:03:23 +01:00
parent 5b8ede3a19
commit ada06c6240
5 changed files with 53 additions and 19 deletions

View File

@@ -7,12 +7,6 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<permission
android:name="com.omixlab.lckcontrol.permission.USE_LCK_CONTROL"
android:label="Access LCK Control Service"
android:description="@string/permission_use_lck_control_desc"
android:protectionLevel="normal" />
<application
android:name=".LckControlApp"
android:allowBackup="true"
@@ -71,7 +65,6 @@
<service
android:name=".service.LckControlService"
android:exported="true"
android:permission="com.omixlab.lckcontrol.permission.USE_LCK_CONTROL"
android:foregroundServiceType="connectedDevice">
<intent-filter>
<action android:name="com.omixlab.lckcontrol.BIND" />

View File

@@ -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,8 +15,17 @@ class TokenStore @Inject constructor(
) {
private val masterKey = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
private val prefs = EncryptedSharedPreferences.create(
"lck_control_tokens",
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,
@@ -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"
}

View File

@@ -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<String, ConnectedClient>()
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<ConnectedClient> {
val removed = mutableListOf<ConnectedClient>()
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)

View File

@@ -57,7 +57,18 @@ class LckControlService : Service() {
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val clientTracker = ClientTracker()
private val callbacks = RemoteCallbackList<ILckControlCallback>()
private val callbacks = object : RemoteCallbackList<ILckControlCallback>() {
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) {

View File

@@ -7,7 +7,7 @@ android {
compileSdk = 36
defaultConfig {
minSdk = 33
minSdk = 32
}
compileOptions {