Prevent crash and better discovery page

This commit is contained in:
Harish Vishwakarma 2026-01-29 17:10:13 +05:30
parent c65322fbf7
commit d783455ed8
4 changed files with 81 additions and 23 deletions

View File

@ -72,8 +72,8 @@ android {
applicationId = "com.zekochat"
minSdk = 24
targetSdk = 36
versionCode = Integer.parseInt("001_003_107".replace("_", ""), 10)
versionName = "1.3.6b"
versionCode = Integer.parseInt("001_003_109".replace("_", ""), 10)
versionName = "1.3.6d"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -1,12 +1,18 @@
package chat.zekochat
import android.app.Application
import android.os.Looper
import android.util.Log
import chat.zekochat.api.PeptideHttp
import chat.zekochat.api.realtime.DisconnectionState
import chat.zekochat.api.realtime.RealtimeSocket
import chat.zekochat.persistence.KVStorage
import com.google.android.material.color.DynamicColors
import dagger.hilt.android.HiltAndroidApp
import logcat.AndroidLogcatLogger
import logcat.LogPriority
import java.io.IOException
import java.net.SocketException
@HiltAndroidApp
class PeptideApplication : Application() {
@ -17,9 +23,57 @@ class PeptideApplication : Application() {
override fun onCreate() {
super.onCreate()
AndroidLogcatLogger.installOnDebuggableApp(this, minPriority = LogPriority.VERBOSE)
installNetworkUncaughtExceptionHandler()
PeptideHttp // Trigger initialization
}
/**
* Prevents app crashes when OkHttp's WebSocket reader thread throws uncaught
* network exceptions (e.g. SocketException: Software caused connection abort)
* that are not propagated to our coroutine try/catch in connectWS().
*/
private fun installNetworkUncaughtExceptionHandler() {
val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
if (isNetworkRelatedFromBackgroundThread(thread, throwable)) {
Log.w(
"PeptideApplication",
"Suppressing uncaught network exception from background thread (e.g. WebSocket connection abort): ${throwable.message}",
throwable
)
try {
if (isWebSocketRelated(throwable)) {
RealtimeSocket.updateDisconnectionState(DisconnectionState.Disconnected)
}
} catch (_: Throwable) {
// Avoid double-fault when updating state
}
return@setDefaultUncaughtExceptionHandler
}
previousHandler?.uncaughtException(thread, throwable)
}
}
private fun isNetworkRelatedFromBackgroundThread(thread: Thread, throwable: Throwable): Boolean {
if (thread == Looper.getMainLooper().thread) return false
var t: Throwable? = throwable
while (t != null) {
when (t) {
is SocketException -> return true
is IOException -> return true
else -> t = t.cause
}
}
return false
}
private fun isWebSocketRelated(throwable: Throwable): Boolean {
val stack = throwable.stackTraceToString()
return stack.contains("okhttp3.internal.ws") ||
stack.contains("WebSocketReader") ||
stack.contains("RealWebSocket")
}
init {
instance = this
DynamicColors.applyToActivitiesIfAvailable(this)

View File

@ -50,6 +50,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.json.Json
import java.io.IOException
import java.net.SocketException
import chat.zekochat.api.schemas.Channel as ChannelSchema
@ -314,6 +315,9 @@ object PeptideAPI {
} catch (e: SocketException) {
Log.d("PeptideAPI", "Socket closed, probably no big deal /// " + e.message)
RealtimeSocket.updateDisconnectionState(DisconnectionState.Disconnected)
} catch (e: IOException) {
Log.d("PeptideAPI", "WebSocket connection abort/reset (network): " + e.message)
RealtimeSocket.updateDisconnectionState(DisconnectionState.Disconnected)
} catch (e: Exception) {
Log.e("PeptideAPI", "WebSocket error", e)
RealtimeSocket.updateDisconnectionState(DisconnectionState.Disconnected)

View File

@ -30,6 +30,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -71,31 +72,30 @@ fun DiscoverServersList(
Column(
modifier = Modifier
.padding(top = 24.dp, start = 16.dp, end = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
.padding(top = 12.dp, start = 16.dp, end = 16.dp),
) {
Image(
modifier = Modifier
.padding(8.dp),
painter = painterResource(R.drawable.discover_character_image),
contentDescription = null,
)
Text(
text = stringResource(R.string.discover_servers),
style = MaterialTheme.typography.headlineMedium
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Image(
modifier = Modifier
.size(72.dp)
.padding(4.dp),
painter = painterResource(R.drawable.discover_character_image),
contentDescription = null,
contentScale = ContentScale.Fit,
)
Text(
text = stringResource(R.string.discover_servers_description),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.weight(1f),
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.discover_servers_description),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(16.dp))
when {
uiState.isLoading -> {
Box(