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:
@@ -7,12 +7,6 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<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
|
<application
|
||||||
android:name=".LckControlApp"
|
android:name=".LckControlApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -71,7 +65,6 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".service.LckControlService"
|
android:name=".service.LckControlService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="com.omixlab.lckcontrol.permission.USE_LCK_CONTROL"
|
|
||||||
android:foregroundServiceType="connectedDevice">
|
android:foregroundServiceType="connectedDevice">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.omixlab.lckcontrol.BIND" />
|
<action android:name="com.omixlab.lckcontrol.BIND" />
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.omixlab.lckcontrol.data.local
|
package com.omixlab.lckcontrol.data.local
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import androidx.security.crypto.MasterKeys
|
import androidx.security.crypto.MasterKeys
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
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 masterKey = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
|
||||||
|
|
||||||
private val prefs = EncryptedSharedPreferences.create(
|
private val prefs: SharedPreferences = try {
|
||||||
"lck_control_tokens",
|
createEncryptedPrefs(context)
|
||||||
masterKey,
|
} catch (e: Exception) {
|
||||||
context,
|
Log.w("TokenStore", "Corrupted keyset, clearing and recreating", e)
|
||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit().clear().commit()
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
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? =
|
fun getJwt(): String? =
|
||||||
prefs.getString(KEY_JWT, null)
|
prefs.getString(KEY_JWT, null)
|
||||||
@@ -44,6 +55,7 @@ class TokenStore @Inject constructor(
|
|||||||
fun isLoggedIn(): Boolean = getJwt() != null
|
fun isLoggedIn(): Boolean = getJwt() != null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val PREFS_NAME = "lck_control_tokens"
|
||||||
private const val KEY_JWT = "session_jwt"
|
private const val KEY_JWT = "session_jwt"
|
||||||
private const val KEY_REFRESH_TOKEN = "session_refresh_token"
|
private const val KEY_REFRESH_TOKEN = "session_refresh_token"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ data class ConnectedClient(
|
|||||||
val clientId: String,
|
val clientId: String,
|
||||||
val clientName: String,
|
val clientName: String,
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
|
val callingUid: Int = 0,
|
||||||
val activePlanId: String? = null,
|
val activePlanId: String? = null,
|
||||||
val connectedAt: Long = System.currentTimeMillis(),
|
val connectedAt: Long = System.currentTimeMillis(),
|
||||||
)
|
)
|
||||||
@@ -15,18 +16,33 @@ class ClientTracker {
|
|||||||
|
|
||||||
private val clients = ConcurrentHashMap<String, ConnectedClient>()
|
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()
|
val clientId = UUID.randomUUID().toString()
|
||||||
clients[clientId] = ConnectedClient(
|
clients[clientId] = ConnectedClient(
|
||||||
clientId = clientId,
|
clientId = clientId,
|
||||||
clientName = clientName,
|
clientName = clientName,
|
||||||
packageName = packageName,
|
packageName = packageName,
|
||||||
|
callingUid = callingUid,
|
||||||
)
|
)
|
||||||
return clientId
|
return clientId
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregister(clientId: String): ConnectedClient? = clients.remove(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?) {
|
fun setActivePlan(clientId: String, planId: String?) {
|
||||||
clients.computeIfPresent(clientId) { _, client ->
|
clients.computeIfPresent(clientId) { _, client ->
|
||||||
client.copy(activePlanId = planId)
|
client.copy(activePlanId = planId)
|
||||||
|
|||||||
@@ -57,7 +57,18 @@ class LckControlService : Service() {
|
|||||||
|
|
||||||
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||||
private val clientTracker = ClientTracker()
|
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() {
|
private val binder = object : ILckControlService.Stub() {
|
||||||
|
|
||||||
@@ -146,7 +157,8 @@ class LckControlService : Service() {
|
|||||||
// ── Clients ─────────────────────────────────────────
|
// ── Clients ─────────────────────────────────────────
|
||||||
|
|
||||||
override fun registerClient(clientName: String, packageName: String): String {
|
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)
|
broadcastClientRegistered(clientId)
|
||||||
return clientId
|
return clientId
|
||||||
}
|
}
|
||||||
@@ -174,7 +186,8 @@ class LckControlService : Service() {
|
|||||||
// ── Callbacks ───────────────────────────────────────
|
// ── Callbacks ───────────────────────────────────────
|
||||||
|
|
||||||
override fun registerCallback(callback: ILckControlCallback) {
|
override fun registerCallback(callback: ILckControlCallback) {
|
||||||
callbacks.register(callback)
|
val uid = android.os.Binder.getCallingUid()
|
||||||
|
callbacks.register(callback, uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unregisterCallback(callback: ILckControlCallback) {
|
override fun unregisterCallback(callback: ILckControlCallback) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ android {
|
|||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 33
|
minSdk = 32
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
|||||||
Reference in New Issue
Block a user