1package app.whiskers 2 3import uniffi.whiskers_ffi.Outcome 4import uniffi.whiskers_ffi.WhiskersEngine 5import java.io.File 6import java.time.Instant 7import java.time.LocalDate 8import java.time.ZoneId 9import java.time.format.DateTimeFormatter 10 11/** The real [Parents]: today's log, read back from the core. */ 12class EngineParents( 13 private val engine: WhiskersEngine, 14 private val address: ServiceAddress, 15 private val zone: ZoneId = ZoneId.systemDefault(), 16) : Parents { 17 private fun bounds(): Pair<Long, Long> { 18 val d = LocalDate.now(zone) 19 return d.atStartOfDay(zone).toInstant().toEpochMilli() to d.plusDays(1).atStartOfDay(zone).toInstant().toEpochMilli() 20 } 21 22 override fun today(): ParentView { 23 val (from, to) = bounds() 24 val clock = DateTimeFormatter.ofPattern("h:mm a").withZone(zone) 25 val began = System.nanoTime() 26 return try { 27 val d = engine.digest(from.toULong(), to.toULong()) 28 Wlog.d(Area.Parents, "today: ${d.exchanges.size} exchanges, ${d.answered} answered, ${d.stopped} stopped, ${d.unreadableLines} unreadable, ${d.factsLearned.size} facts learned, in ${(System.nanoTime() - began) / 1_000_000} ms") 29 ParentView( 30 day = DateTimeFormatter.ofPattern("EEEE, MMMM d").format(LocalDate.now(zone)), 31 answered = d.answered.toInt(), 32 stopped = d.stopped.toInt(), 33 lines = d.exchanges.map { 34 Line( 35 time = clock.format(Instant.ofEpochMilli(it.atMs.toLong())), 36 heard = it.heard, 37 said = it.said, 38 urgent = it.needsAGrownUp, 39 notes = it.notes, 40 withheld = if (it.outcome == Outcome.ANSWERED) null else it.modelWrote, 41 pictures = it.pictures.map { id -> File(engine.picturePath(id)) }, 42 ) 43 }, 44 factsLearned = d.factsLearned, 45 facts = engine.facts().map { FactLine(it.id.toLong(), it.text, it.kind, it.who, it.place, it.`when`, it.pictures.map { id -> File(engine.picturePath(id)) }, it.hiddenByChild, it.icon) }, 46 unreadableLines = d.unreadableLines.toInt(), 47 ) 48 } catch (e: Exception) { 49 Wlog.e(Area.Parents, "today: the log could not be read after ${(System.nanoTime() - began) / 1_000_000} ms", e) 50 // The log could not be read, but what she told it is a separate file and still there. 51 val facts = runCatching { engine.facts().map { FactLine(it.id.toLong(), it.text, it.kind, it.who, it.place, it.`when`, it.pictures.map { id -> File(engine.picturePath(id)) }, it.hiddenByChild, it.icon) } } 52 .onFailure { Wlog.e(Area.Parents, "today: the facts could not be read either", it) } 53 .getOrDefault(emptyList()) 54 ParentView(day = "", answered = 0, stopped = 0, lines = emptyList(), factsLearned = emptyList(), facts = facts, error = e.message ?: "could not read the log") 55 } 56 } 57 58 override fun note(): String { 59 val (from, to) = bounds() 60 val began = System.nanoTime() 61 Wlog.d(Area.Parents, "note: summarising the day") 62 return try { 63 engine.summarize(from.toULong(), to.toULong()).also { Wlog.i(Area.Parents, "note: ${it.length} chars in ${(System.nanoTime() - began) / 1_000_000} ms") } 64 } catch (e: Exception) { 65 Wlog.e(Area.Parents, "note: summarising failed after ${(System.nanoTime() - began) / 1_000_000} ms", e) 66 throw e 67 } 68 } 69 70 override fun voiceUsage(): VoiceUsage? = fetchVoiceUsage(address) 71 72 private fun day() = LocalDate.now(zone).toEpochDay().toUInt() 73 74 override fun settings(): HouseholdSettings = engine.householdSettings().also { Wlog.v(Area.Parents, "settings read") }.let { 75 HouseholdSettings( 76 it.dailyMinutes?.toInt(), it.quietFrom?.toInt(), it.quietUntil?.toInt(), 77 it.tokensPerWindow?.toLong(), it.windowHours.toInt(), it.keepMicOpen, it.voiceDailyChars.toInt(), it.pinHash, 78 it.child?.let { c -> ChildProfile(c.name, c.ageYears.toInt()) }, 79 it.catTheme.toApp(), 80 ) 81 } 82 83 override fun setSettings(settings: HouseholdSettings) { 84 Wlog.i(Area.Parents, "setSettings: dailyMinutes=${settings.dailyMinutes}, quiet=${settings.quietFrom}-${settings.quietUntil}, tokens=${settings.tokensPerWindow}/${settings.windowHours}h, keepMicOpen=${settings.keepMicOpen}, voiceDailyChars=${settings.voiceDailyChars}, pinSet=${settings.pinHash != null}, childSet=${settings.child != null}, catTheme=${settings.catTheme}") 85 engine.setHouseholdSettings( 86 uniffi.whiskers_ffi.HouseholdSettings( 87 settings.dailyMinutes?.toUInt(), settings.quietFrom?.toUShort(), settings.quietUntil?.toUShort(), 88 settings.tokensPerWindow?.toULong(), settings.windowHours.toULong(), settings.keepMicOpen, 89 settings.voiceDailyChars.toUInt(), settings.pinHash, 90 settings.child?.let { c -> uniffi.whiskers_ffi.ChildProfile(c.name, c.ageYears.toUByte()) }, 91 settings.catTheme.toCore(), 92 ), 93 ) 94 } 95 96 override val childAges: IntRange = uniffi.whiskers_ffi.childAgeRange().let { it.youngest.toInt()..it.oldest.toInt() } 97 98 override fun checkChild(name: String, ageYears: Int): ChildCheck { 99 // Never log the name. The core's rejection says what is wrong without quoting it. 100 if (ageYears !in 0..255) return ChildCheck.Invalid("Choose an age.") 101 return try { 102 val c = uniffi.whiskers_ffi.validateChild(name, ageYears.toUByte()) 103 Wlog.d(Area.Parents, "checkChild: accepted") 104 ChildCheck.Valid(ChildProfile(c.name, c.ageYears.toInt())) 105 } catch (e: uniffi.whiskers_ffi.EngineException) { 106 Wlog.d(Area.Parents, "checkChild: rejected") 107 ChildCheck.Invalid(e.message ?: "That cannot be used.") 108 } 109 } 110 111 override fun sync() { 112 val began = System.nanoTime() 113 runCatching { NetActivity.around { engine.sync() } } 114 .onSuccess { Wlog.d(Area.Sync, "parents sync ok: $it changes in ${(System.nanoTime() - began) / 1_000_000} ms") } 115 .onFailure { Wlog.w(Area.Sync, "parents sync failed after ${(System.nanoTime() - began) / 1_000_000} ms", it) } 116 } 117 118 override fun grantTime(minutes: Int) { 119 Wlog.i(Area.Parents, "grantTime: $minutes minutes") 120 engine.grantTime(day(), minutes.toUInt()) 121 } 122 123 override fun timeUsedToday() = engine.timeUsedMinutes(day()).toInt() 124 125 override fun restore(id: Long): Boolean = engine.restore(id.toULong()).also { 126 Wlog.i(Area.Parents, "restore fact id=$id: restored=$it") 127 runCatching { engine.sync() }.onFailure { e -> Wlog.w(Area.Sync, "sync after restore failed", e) } 128 } 129 130 override fun forget(id: Long): Boolean = engine.forget(id.toULong()).also { 131 Wlog.i(Area.Parents, "forget fact id=$id: removed=$it") 132 runCatching { engine.sync() }.onFailure { e -> Wlog.w(Area.Sync, "sync after forget failed", e) } 133 } 134} 135 136private fun uniffi.whiskers_ffi.CatTheme.toApp(): CatTheme = when (this) { 137 uniffi.whiskers_ffi.CatTheme.GREY -> CatTheme.Grey 138 uniffi.whiskers_ffi.CatTheme.GINGER -> CatTheme.Ginger 139} 140 141private fun CatTheme.toCore(): uniffi.whiskers_ffi.CatTheme = when (this) { 142 CatTheme.Grey -> uniffi.whiskers_ffi.CatTheme.GREY 143 CatTheme.Ginger -> uniffi.whiskers_ffi.CatTheme.GINGER 144}