014437760d
- ChatScreen: restructure to Box overlay layout so only input area rises with IME, messages stay fixed. Add opaque background to input area. Use reverseLayout with newest-first animation order. - OverlayContent: remove all manual IME detection — system forces adjust=pan on VoiceInteractionSession windows, so manual padding caused double offset. Let system handle IME naturally. - ChatRepositoryImpl: add messageRemovals flow to clean up wrapping stream_end/response when review/multi_message items arrive later. Track lastResponseId in both stream_end and response handlers. - ChatViewModel/OverlayViewModel: fix dedup to check by message ID only. Sort descending (newest first). Observe messageRemovals. - NavGraph: keep all tabs composed with graphicsLayer alpha toggle — prevents ChatScreen destruction and re-render on tab switch. - CyreneVoiceInteractionSession: defer configureWindow via post() to override system softInputMode flags. - AndroidManifest: set windowSoftInputMode=adjustNothing on main activity. - Add WebSocketKeepAliveService for background connection persistence. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
101 lines
3.5 KiB
Kotlin
101 lines
3.5 KiB
Kotlin
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 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)
|
|
@Volatile
|
|
private var notificationHelper: NotificationHelper? = null
|
|
|
|
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")
|
|
notificationHelper?.cancelAll()
|
|
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 helper = NotificationHelper(this@CyreneApplication)
|
|
notificationHelper = helper
|
|
val repo = getRepo()
|
|
repo?.setNotificationCallback { message ->
|
|
helper.showMessageNotification(message)
|
|
}
|
|
}
|
|
|
|
initScope.launch {
|
|
val koin = GlobalContext.get()
|
|
val prefs: PreferencesDataStore = koin.get()
|
|
val urlInterceptor: DynamicUrlInterceptor = koin.get()
|
|
val authInterceptor: AuthInterceptor = koin.get()
|
|
|
|
prefs.baseUrl.firstOrNull()?.let { url ->
|
|
if (url.isNotBlank()) urlInterceptor.baseUrl = url
|
|
}
|
|
prefs.token.firstOrNull()?.let { token ->
|
|
authInterceptor.token = token
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun getRepo(): ChatRepositoryImpl? {
|
|
return try {
|
|
GlobalContext.get().get()
|
|
} catch (_: Throwable) {
|
|
null
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private const val TAG = "CyreneApp"
|
|
}
|
|
}
|