A pass over every Kotlin file, fixing each place a rule the codebase had already learned was applied to only one member of its set: - Notifications: checkSelfPermission(POST_NOTIFICATIONS) on Android 12 and below answers "denied" for a permission that does not exist there, so every notification on API 24-32 was silently dropped. Version-guarded; before 13 the app-level switch is the whole answer. - The permission-mode list was written three times and had drifted: the import screen was missing "plan". One list in Api.kt now. - The import screen's delete refetched the whole list through a loading spinner -- the exact fault the session list's delete already fixed and documented. It now removes only the deleted row. - The import screen truncated paths at a hardcoded 40 characters; it now uses StartEllipsis against the row's real width, like the models screen. - warm() bypassed the partsOf cache built for it, re-scanning every loaded message per page, and warmed a multi-block memory note under keys no row looks up. It now mirrors transcriptUnits through the same caches. - The transcript's data model (TranscriptItem, foldEvent, joinPages, warm) moved out of SessionScreen.kt into TranscriptItems.kt: pure folding with no screen in it, changing for unrelated reasons in the same file. - One image fetch/decode/failed block was written twice; it is rememberSessionBitmap in SessionImage.kt now. - Lint is fully clean: android.media.ExifInterface replaced with the androidx one (the framework copy lacks the hostile-image parsing fixes, and these images arrive from outside the phone), highlights bumped to 1.1.0, and the notification fix above closed InlinedApi. - Dead weight out: an unused act() onFailure parameter, and five orphaned or misattached doc comments (UserBubble carried SessionImage's doc). Verified: ktfmt, compileDebugKotlin, lintDebug (0 issues), server suite (93 passed), clippy and rustfmt clean; exercised on the emulator against a real imported transcript -- paging, tool groups, block rendering, no crashes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
58 lines
2.6 KiB
Kotlin
58 lines
2.6 KiB
Kotlin
package com.example.aiapp
|
|
|
|
import java.time.Duration
|
|
import java.time.OffsetDateTime
|
|
|
|
// How long is left in a usage window. Shared by the session bar and the usage screen: the
|
|
// arithmetic is the same in both and only the sentence around it differs, so everything here
|
|
// returns the span or the state on its own and leaves the wording to the caller.
|
|
|
|
/** "1d 4h", "3h 12m", "12m" -- the span alone, with no leading or trailing words. */
|
|
fun formatSpan(until: Duration): String =
|
|
when {
|
|
until.toHours() >= 24 -> "${until.toDays()}d ${until.toHours() % 24}h"
|
|
until.toHours() > 0 -> "${until.toHours()}h ${until.toMinutes() % 60}m"
|
|
else -> "${until.toMinutes()}m"
|
|
}
|
|
|
|
/**
|
|
* What is known about when a usage window ends.
|
|
*
|
|
* Three answers rather than a nullable duration, because two of them shared `null` and they are not
|
|
* the same thing at all. A window the server sent no reset time for is one that is **not running**:
|
|
* the five-hour window is anchored to the block it started in, so between sessions there is nothing
|
|
* counting down and the API says so by omitting the field -- measured against a live response on
|
|
* 2026-08-31, where the five-hour window's reset was exactly five hours after the moment work
|
|
* resumed. A timestamp that did arrive and could not be read is the genuinely unknown case, and it
|
|
* is the only one worth those words.
|
|
*
|
|
* Collapsing them put "reset time unknown" on the session bar for a machine behaving perfectly, on
|
|
* the one row somebody reads before starting something big -- and the usage dialog, looking at the
|
|
* same field, quietly drew nothing. Two rules for one missing value; this is the rule.
|
|
*/
|
|
sealed class WindowEnd {
|
|
/** No reset time was sent, so nothing is running in this window. Not a failure to find out. */
|
|
data object NotRunning : WindowEnd()
|
|
|
|
/** A timestamp arrived and could not be read. The one case that is actually unknown. */
|
|
data object Unreadable : WindowEnd()
|
|
|
|
/** How long is left. Negative once the window is past, which each caller words for itself. */
|
|
data class Ends(val until: Duration) : WindowEnd()
|
|
}
|
|
|
|
/**
|
|
* [resetsAt] as the server sent it -- absent, unreadable, or a moment -- against [now].
|
|
*
|
|
* [now] is a parameter rather than read here so a caller can drive it from state and have the
|
|
* countdown recompute on its own schedule.
|
|
*/
|
|
fun windowEnd(resetsAt: String?, now: OffsetDateTime): WindowEnd {
|
|
if (resetsAt == null) return WindowEnd.NotRunning
|
|
return try {
|
|
WindowEnd.Ends(Duration.between(now, OffsetDateTime.parse(resetsAt)))
|
|
} catch (_: Exception) {
|
|
WindowEnd.Unreadable
|
|
}
|
|
}
|