feat: reconnection, overlay UI, profile caching, history loading, log viewer, and about page

- Reconnection: unlimited retries with capped backoff, forceReconnect on foreground
  and manual refresh when offline
- Overlay: fix status bar coverage, remove scrim, fix IME layout (messages fixed
  at top, only full-screen IME pushes input), handle process-kill by eager
  ViewModel resolution with try-catch
- Profile: cache-first rendering, cloud refresh on each visit, silent fallback
  to cache on failure
- Messages: fix message swallowing by tracking DB observer job and using atomic
  StateFlow.update(), add dedicated isAssistantStreaming state for reliable
  typing indicator
- History: history_response handler now emits to live message stream, HTTP
  fallback waits for WS connection before requesting history
- Foreground: always request history on foreground to catch cross-device messages
- Log viewer: enhanced with All tab, auto-scroll, new categories (HTTP, voice, general),
  added log points for app lifecycle and overlay events
- Settings: added About page with project link (https://git.yeij.top/AskaEth/Cyrene-For-Android)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 17:58:34 +08:00
parent a57692353c
commit 367ef7f2d6
48 changed files with 4439 additions and 540 deletions
@@ -1,32 +1,74 @@
package top.yeij.cyrene
import android.app.Activity
import android.app.Application
import android.os.Bundle
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.GlobalContext
import org.koin.core.context.startKoin
import top.yeij.cyrene.data.local.PreferencesDataStore
import top.yeij.cyrene.data.remote.AuthInterceptor
import top.yeij.cyrene.data.remote.DynamicUrlInterceptor
import top.yeij.cyrene.data.repository.ChatRepositoryImpl
import top.yeij.cyrene.di.appModule
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import top.yeij.cyrene.util.NotificationHelper
import top.yeij.cyrene.util.RuntimeLog
import java.util.concurrent.atomic.AtomicInteger
class CyreneApplication : Application() {
private val initScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val activityCount = AtomicInteger(0)
override fun onCreate() {
super.onCreate()
RuntimeLog.general("app", "Application onCreate")
startKoin {
androidContext(this@CyreneApplication)
modules(appModule)
}
// Track foreground/background state
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityStarted(activity: Activity) {
if (activityCount.incrementAndGet() == 1) {
RuntimeLog.general("app", "App in foreground")
getRepo()?.onAppForeground()
}
}
override fun onActivityStopped(activity: Activity) {
if (activityCount.decrementAndGet() == 0) {
RuntimeLog.general("app", "App in background")
getRepo()?.onAppBackground()
}
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
})
// Set up background notification callback once Koin is ready
initScope.launch {
val koin = org.koin.core.context.GlobalContext.get()
val notificationHelper = NotificationHelper(this@CyreneApplication)
val repo = getRepo()
repo?.setNotificationCallback { message ->
notificationHelper.showMessageNotification(message)
}
}
initScope.launch {
val koin = GlobalContext.get()
val prefs: PreferencesDataStore = koin.get()
val urlInterceptor: DynamicUrlInterceptor = koin.get()
val authInterceptor: AuthInterceptor = koin.get()
@@ -39,4 +81,16 @@ class CyreneApplication : Application() {
}
}
}
private fun getRepo(): ChatRepositoryImpl? {
return try {
GlobalContext.get().get()
} catch (_: Exception) {
null
}
}
companion object {
private const val TAG = "CyreneApp"
}
}