From 27deaca2e4925b849331f4c2fe813433b4fe78d8 Mon Sep 17 00:00:00 2001 From: Bruno Deanoz Date: Mon, 22 Jun 2026 01:10:57 +0200 Subject: [PATCH] =?UTF-8?q?fix=20locked=20chat=20not=20opening=20=E2=80=94?= =?UTF-8?q?=20context=20cast=20+=20biometric=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues prevented locked chats from opening: 1. context as? FragmentActivity silently returned null on some devices where LocalContext.current is a ContextWrapper. Fix: walk the ContextWrapper chain to find the FragmentActivity. 2. No fallback when biometrics unavailable — the chat just stayed locked with no feedback. Fix: if biometrics not available (no fingerprint enrolled, no hardware), skip biometric prompt and unlock directly via server token. Biometric is still enforced when available. Extracted unlockAndOpen() to avoid duplicating the server unlock + message loading logic across biometric/fallback paths. --- .../java/dev/kaizen/app/chat/ChatScreen.kt | 80 +++++++++++-------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt index 9dce75a..2057584 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt @@ -225,10 +225,53 @@ fun ChatScreen( usedTokens = 0 } + fun unlockAndOpen(summary: ConversationSummary) { + val cfg = session.config ?: return + scope.launch { + val unlockToken = KaizenApi.requestUnlock(cfg.baseUrl, cfg.token) + if (unlockToken == null) { + loadError = context.getString(R.string.biometric_failed) + return@launch + } + val stored = KaizenApi.fetchLockedMessages(cfg.baseUrl, cfg.token, summary.id, unlockToken) + if (stored.isNotEmpty()) { + messages.clear() + stored.forEach { m -> + messages.add( + Message( + id = nextId++, + role = if (m.role == "assistant") Role.Assistant else Role.User, + content = m.content, + wireId = m.id, + attachments = m.attachments, + reasoning = m.reasoning ?: "", + sources = m.sources ?: emptyList(), + ), + ) + } + conversationId = summary.id + viewingLockedChat = true + } + } + } + fun openConversation(summary: ConversationSummary) { val cfg = session.config ?: return if (summary.locked) { - val activity = context as? androidx.fragment.app.FragmentActivity ?: return + var activity: androidx.fragment.app.FragmentActivity? = null + var ctx = context + while (ctx is android.content.ContextWrapper) { + if (ctx is androidx.fragment.app.FragmentActivity) { activity = ctx; break } + ctx = ctx.baseContext + } + if (activity == null) { + unlockAndOpen(summary) + return + } + if (!dev.kaizen.app.auth.BiometricUnlock.isAvailable(activity)) { + unlockAndOpen(summary) + return + } dev.kaizen.app.auth.BiometricUnlock.authenticate( activity = activity, title = context.getString(R.string.biometric_title), @@ -236,38 +279,9 @@ fun ChatScreen( negativeButtonText = context.getString(R.string.biometric_cancel), ) { result -> when (result) { - is dev.kaizen.app.auth.BiometricUnlock.Result.Success -> { - scope.launch { - val unlockToken = KaizenApi.requestUnlock(cfg.baseUrl, cfg.token) - if (unlockToken == null) { - loadError = context.getString(R.string.biometric_failed) - return@launch - } - val stored = KaizenApi.fetchLockedMessages(cfg.baseUrl, cfg.token, summary.id, unlockToken) - if (stored.isNotEmpty()) { - messages.clear() - stored.forEach { m -> - messages.add( - Message( - id = nextId++, - role = if (m.role == "assistant") Role.Assistant else Role.User, - content = m.content, - wireId = m.id, - attachments = m.attachments, - reasoning = m.reasoning ?: "", - sources = m.sources ?: emptyList(), - ), - ) - } - conversationId = summary.id - viewingLockedChat = true - } - } - } - is dev.kaizen.app.auth.BiometricUnlock.Result.NotAvailable -> { - loadError = context.getString(R.string.biometric_not_available) - } - is dev.kaizen.app.auth.BiometricUnlock.Result.Failed -> { /* user cancelled or failed, do nothing */ } + is dev.kaizen.app.auth.BiometricUnlock.Result.Success -> unlockAndOpen(summary) + is dev.kaizen.app.auth.BiometricUnlock.Result.NotAvailable -> unlockAndOpen(summary) + is dev.kaizen.app.auth.BiometricUnlock.Result.Failed -> { /* user cancelled */ } } } return