1package com.peachpasswords.sync23import android.content.Context4import androidx.security.crypto.EncryptedSharedPreferences5import androidx.security.crypto.MasterKey6import dagger.hilt.android.qualifiers.ApplicationContext7import kotlinx.coroutines.flow.MutableStateFlow8import kotlinx.coroutines.flow.StateFlow9import kotlinx.coroutines.flow.asStateFlow10import kotlinx.serialization.json.Json11import javax.inject.Inject12import javax.inject.Singleton1314151617181920212223@Singleton24class SyncOutcomeStore @Inject constructor(25 @ApplicationContext context: Context,26) {27 private val masterKey = MasterKey.Builder(context)28 .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)29 .build()3031 private val prefs = EncryptedSharedPreferences.create(32 context,33 PREFS_FILE,34 masterKey,35 EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,36 EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,37 )3839 private val _summary = MutableStateFlow(decodeSyncOutcome(prefs.getString(KEY_LAST, null)))40 val summary: StateFlow<SyncOutcomeSummary> = _summary.asStateFlow()4142 43 fun begin() {44 _summary.value = _summary.value.copy(syncing = true)45 }4647 48495051 fun report(52 reason: SyncOutcomeReason,53 detail: String? = null,54 entriesSent: Int = 0,55 entriesReceived: Int = 0,56 ) {57 val next = SyncOutcomeSummary(58 reason = reason,59 detail = detail,60 entriesSent = entriesSent,61 entriesReceived = entriesReceived,62 finishedAt = System.currentTimeMillis(),63 )64 prefs.edit().putString(KEY_LAST, encodeSyncOutcome(next)).apply()65 _summary.value = next66 }6768 69 fun reset() {70 prefs.edit().remove(KEY_LAST).apply()71 _summary.value = SyncOutcomeSummary()72 }7374 private companion object {75 const val PREFS_FILE = "peach_sync_outcome"76 const val KEY_LAST = "last_outcome"77 }78}7980private val outcomeJson = Json { encodeDefaults = true; ignoreUnknownKeys = true }8182internal fun encodeSyncOutcome(summary: SyncOutcomeSummary): String =83 outcomeJson.encodeToString(SyncOutcomeSummary.serializer(), summary.copy(syncing = false))848586878889internal fun decodeSyncOutcome(raw: String?): SyncOutcomeSummary {90 if (raw == null) return SyncOutcomeSummary()91 return runCatching {92 outcomeJson.decodeFromString(SyncOutcomeSummary.serializer(), raw).copy(syncing = false)93 }.getOrDefault(SyncOutcomeSummary())94}