1package com.peachpasswords.sync23import android.content.Context4import androidx.security.crypto.EncryptedSharedPreferences5import androidx.security.crypto.MasterKey6import dagger.hilt.android.qualifiers.ApplicationContext7import kotlinx.coroutines.channels.awaitClose8import kotlinx.coroutines.flow.Flow9import kotlinx.coroutines.flow.callbackFlow10import kotlinx.serialization.builtins.ListSerializer11import kotlinx.serialization.json.Json12import javax.inject.Inject13import javax.inject.Singleton141516171819202122232425262728293031323334353637@Singleton38class PairedDeviceStore @Inject constructor(39 @ApplicationContext context: Context,40) : PairedDeviceRegistry {41 private val masterKey = MasterKey.Builder(context)42 .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)43 .build()4445 private val prefs = EncryptedSharedPreferences.create(46 context,47 PREFS_FILE,48 masterKey,49 EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,50 EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,51 )5253 private val listSerializer = ListSerializer(PairedDevice.serializer())5455 override fun getAll(): List<PairedDevice> {56 val raw = prefs.getString(KEY_DEVICES, null) ?: return emptyList()57 return runCatching { json.decodeFromString(listSerializer, raw) }.getOrDefault(emptyList())58 }5960 override fun get(id: String): PairedDevice? = getAll().firstOrNull { it.deviceId == id }6162 override fun getByPubkey(pubkeyHex: String): PairedDevice? =63 getAll().firstOrNull {64 it.longTermPublicKeyHex?.equals(pubkeyHex, ignoreCase = true) == true65 }6667 68 override fun upsert(device: PairedDevice) {69 val next = getAll().filterNot { it.deviceId == device.deviceId } + device70 persist(next)71 }7273 fun remove(id: String) {74 persist(getAll().filterNot { it.deviceId == id })75 }7677 fun clear() {78 prefs.edit().remove(KEY_DEVICES).apply()79 }8081 82838485 fun updateLastSeen(id: String, ip: String? = null) {86 val current = get(id) ?: return87 if (ip == null) return88 upsert(current.copy(lastKnownIp = ip))89 }9091 fun updateLastSync(id: String, at: Long = System.currentTimeMillis()) {92 val current = get(id) ?: return93 upsert(current.copy(lastSyncedAt = at))94 }9596 979899100101 fun observeAll(): Flow<List<PairedDevice>> = callbackFlow {102 trySend(getAll())103 val listener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key ->104 if (key == KEY_DEVICES || key == null) trySend(getAll())105 }106 prefs.registerOnSharedPreferenceChangeListener(listener)107 awaitClose { prefs.unregisterOnSharedPreferenceChangeListener(listener) }108 }109110 private fun persist(devices: List<PairedDevice>) {111 prefs.edit()112 .putString(KEY_DEVICES, json.encodeToString(listSerializer, devices))113 .apply()114 }115116 private companion object {117 const val PREFS_FILE = "peach_paired_devices"118 const val KEY_DEVICES = "paired_devices"119 val json = Json { encodeDefaults = true; ignoreUnknownKeys = true }120 }121}