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 63 override fun upsert(device: PairedDevice) {64 val next = getAll().filterNot { it.deviceId == device.deviceId } + device65 persist(next)66 }6768 fun remove(id: String) {69 persist(getAll().filterNot { it.deviceId == id })70 }7172 fun clear() {73 prefs.edit().remove(KEY_DEVICES).apply()74 }7576 77787980 fun updateLastSeen(id: String, ip: String? = null) {81 val current = get(id) ?: return82 if (ip == null) return83 upsert(current.copy(lastKnownIp = ip))84 }8586 fun updateLastSync(id: String, at: Long = System.currentTimeMillis()) {87 val current = get(id) ?: return88 upsert(current.copy(lastSyncedAt = at))89 }9091 9293949596 fun observeAll(): Flow<List<PairedDevice>> = callbackFlow {97 trySend(getAll())98 val listener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key ->99 if (key == KEY_DEVICES || key == null) trySend(getAll())100 }101 prefs.registerOnSharedPreferenceChangeListener(listener)102 awaitClose { prefs.unregisterOnSharedPreferenceChangeListener(listener) }103 }104105 private fun persist(devices: List<PairedDevice>) {106 prefs.edit()107 .putString(KEY_DEVICES, json.encodeToString(listSerializer, devices))108 .apply()109 }110111 private companion object {112 const val PREFS_FILE = "peach_paired_devices"113 const val KEY_DEVICES = "paired_devices"114 val json = Json { encodeDefaults = true; ignoreUnknownKeys = true }115 }116}