Merge pull request #196 from ouchadam/feature/more-error-handling

More error handling
This commit is contained in:
Adam Brown 2022-10-09 16:07:21 +01:00 committed by GitHub
commit 65957f6854
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 127 additions and 120 deletions

View File

@ -3,6 +3,13 @@ package app.dapk.st.core.extensions
inline fun <T> T?.ifNull(block: () -> T): T = this ?: block()
inline fun <T> ifOrNull(condition: Boolean, block: () -> T): T? = if (condition) block() else null
inline fun <reified T> Any.takeAs(): T? {
return when (this) {
is T -> this
else -> null
}
}
@Suppress("UNCHECKED_CAST")
inline fun <T, T1 : T, T2 : T> Iterable<T>.firstOrNull(predicate: (T) -> Boolean, predicate2: (T) -> Boolean): Pair<T1, T2>? {
var firstValue: T1? = null

View File

@ -1,21 +1,46 @@
package app.dapk.st.design.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun GenericError(retryAction: () -> Unit) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
fun GenericError(message: String = "Something went wrong...", label: String = "Retry", cause: Throwable? = null, action: () -> Unit) {
val moreDetails = cause?.let { "${it::class.java.simpleName}: ${it.message}" }
val openDetailsDialog = remember { mutableStateOf(false) }
if (openDetailsDialog.value) {
AlertDialog(
onDismissRequest = { openDetailsDialog.value = false },
confirmButton = {
Button(onClick = { openDetailsDialog.value = false }) {
Text("OK")
}
},
title = { Text("Details") },
text = {
Text(moreDetails!!)
}
)
}
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Something went wrong...")
Button(onClick = { retryAction() }) {
Text("Retry")
Text(message)
if (moreDetails != null) {
Text("Tap for more details".uppercase(), fontSize = 12.sp, modifier = Modifier.clickable { openDetailsDialog.value = true }.padding(12.dp))
}
Spacer(modifier = Modifier.height(12.dp))
Button(onClick = { action() }) {
Text(label.uppercase())
}
}
}

View File

@ -10,22 +10,27 @@ fun <T : Any> Spider(currentPage: SpiderPage<T>, onNavigate: (SpiderPage<out T>?
val pageCache = remember { mutableMapOf<Route<*>, SpiderPage<out T>>() }
pageCache[currentPage.route] = currentPage
val navigateAndPopStack = {
pageCache.remove(currentPage.route)
onNavigate(pageCache[currentPage.parent])
}
val itemScope = object : SpiderItemScope {
override fun goBack() {
navigateAndPopStack()
}
}
val computedWeb = remember(true) {
mutableMapOf<Route<*>, @Composable (T) -> Unit>().also { computedWeb ->
val scope = object : SpiderScope {
override fun <T> item(route: Route<T>, content: @Composable (T) -> Unit) {
computedWeb[route] = { content(it as T) }
override fun <T> item(route: Route<T>, content: @Composable SpiderItemScope.(T) -> Unit) {
computedWeb[route] = { content(itemScope, it as T) }
}
}
graph.invoke(scope)
}
}
val navigateAndPopStack = {
pageCache.remove(currentPage.route)
onNavigate(pageCache[currentPage.parent])
}
Column {
if (currentPage.hasToolbar) {
Toolbar(
@ -40,7 +45,11 @@ fun <T : Any> Spider(currentPage: SpiderPage<T>, onNavigate: (SpiderPage<out T>?
interface SpiderScope {
fun <T> item(route: Route<T>, content: @Composable (T) -> Unit)
fun <T> item(route: Route<T>, content: @Composable SpiderItemScope.(T) -> Unit)
}
interface SpiderItemScope {
fun goBack()
}
data class SpiderPage<T>(

View File

@ -7,5 +7,6 @@ dependencies {
implementation project(":matrix:services:auth")
implementation project(":matrix:services:profile")
implementation project(":matrix:services:crypto")
implementation project(":design-library")
implementation project(":core")
}

View File

@ -1,7 +1,6 @@
package app.dapk.st.login
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@ -32,6 +31,8 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.dapk.st.core.StartObserving
import app.dapk.st.core.components.CenteredLoading
import app.dapk.st.design.components.GenericError
import app.dapk.st.login.LoginEvent.LoginComplete
import app.dapk.st.login.LoginScreenState.*
@ -49,42 +50,8 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) {
val keyboardController = LocalSoftwareKeyboardController.current
when (val state = loginViewModel.state) {
is Error -> {
val openDetailsDialog = remember { mutableStateOf(false) }
if (openDetailsDialog.value) {
AlertDialog(
onDismissRequest = { openDetailsDialog.value = false },
confirmButton = {
Button(onClick = { openDetailsDialog.value = false }) {
Text("OK")
}
},
title = { Text("Details") },
text = {
Text(state.cause.message ?: "Unknown")
}
)
}
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Something went wrong")
Text("Tap for more details".uppercase(), fontSize = 12.sp, modifier = Modifier.clickable { openDetailsDialog.value = true }.padding(12.dp))
Spacer(modifier = Modifier.height(12.dp))
Button(onClick = {
loginViewModel.start()
}) {
Text("Retry".uppercase())
}
}
}
}
Loading -> {
Box(contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is Error -> GenericError(cause = state.cause, action = { loginViewModel.start() })
Loading -> CenteredLoading()
is Content ->
Row {

View File

@ -43,10 +43,7 @@ import app.dapk.st.core.LifecycleEffect
import app.dapk.st.core.StartObserving
import app.dapk.st.core.components.CenteredLoading
import app.dapk.st.core.extensions.takeIfContent
import app.dapk.st.design.components.MessengerUrlIcon
import app.dapk.st.design.components.MissingAvatarIcon
import app.dapk.st.design.components.SmallTalkTheme
import app.dapk.st.design.components.Toolbar
import app.dapk.st.design.components.*
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.UserId
import app.dapk.st.matrix.sync.MessageMeta
@ -95,7 +92,7 @@ internal fun MessengerScreen(
})
when (state.composerState) {
is ComposerState.Text -> {
Room(state.roomState, replyActions)
Room(state.roomState, replyActions, onRetry = { viewModel.post(MessengerAction.OnMessengerVisible(roomId, attachments)) })
TextComposer(
state.composerState,
onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) },
@ -132,7 +129,7 @@ private fun MessengerViewModel.ObserveEvents(galleryLauncher: ActivityResultLaun
}
@Composable
private fun ColumnScope.Room(roomStateLce: Lce<MessengerState>, replyActions: ReplyActions) {
private fun ColumnScope.Room(roomStateLce: Lce<MessengerState>, replyActions: ReplyActions, onRetry: () -> Unit) {
when (val state = roomStateLce) {
is Lce.Loading -> CenteredLoading()
is Lce.Content -> {
@ -165,16 +162,7 @@ private fun ColumnScope.Room(roomStateLce: Lce<MessengerState>, replyActions: Re
}
}
is Lce.Error -> {
Box(contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Something went wrong...")
Button(onClick = {}) {
Text("Retry")
}
}
}
}
is Lce.Error -> GenericError(cause = state.cause, action = onRetry)
}
}

View File

@ -15,6 +15,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.lifecycleScope
import app.dapk.st.core.*
import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.design.components.GenericError
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ -59,7 +60,10 @@ fun Activity.PermissionGuard(state: State<Lce<PermissionResult>>, onGranted: @Co
PermissionResult.ShowRational -> finish()
}
is Lce.Error -> finish()
is Lce.Error -> GenericError(message = "Store permission required", label = "Close") {
finish()
}
is Lce.Loading -> {
// loading should be quick, let's avoid displaying anything
}

View File

@ -42,20 +42,17 @@ fun ImageGalleryScreen(viewModel: ImageGalleryViewModel, onTopLevelBack: () -> U
Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) {
item(ImageGalleryPage.Routes.folders) {
ImageGalleryFolders(it) { folder ->
viewModel.selectFolder(folder)
}
ImageGalleryFolders(it, onClick = { viewModel.selectFolder(it) }, onRetry = { viewModel.start() })
}
item(ImageGalleryPage.Routes.files) {
ImageGalleryMedia(it, onImageSelected)
ImageGalleryMedia(it, onImageSelected, onRetry = { viewModel.selectFolder(it.folder) })
}
}
}
@Composable
fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Unit) {
fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Unit, onRetry: () -> Unit) {
val screenWidth = LocalConfiguration.current.screenWidthDp
val gradient = Brush.verticalGradient(
@ -106,12 +103,12 @@ fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Un
}
}
is Lce.Error -> GenericError { }
is Lce.Error -> GenericError(cause = content.cause, action = onRetry)
}
}
@Composable
fun ImageGalleryMedia(state: ImageGalleryPage.Files, onFileSelected: (Media) -> Unit) {
fun ImageGalleryMedia(state: ImageGalleryPage.Files, onFileSelected: (Media) -> Unit, onRetry: () -> Unit) {
val screenWidth = LocalConfiguration.current.screenWidthDp
Column {
@ -149,7 +146,7 @@ fun ImageGalleryMedia(state: ImageGalleryPage.Files, onFileSelected: (Media) ->
}
}
is Lce.Error -> GenericError { }
is Lce.Error -> GenericError(cause = content.cause, action = onRetry)
}
}

View File

@ -48,7 +48,7 @@ class ImageGalleryViewModel(
route = ImageGalleryPage.Routes.files,
label = page.label,
parent = ImageGalleryPage.Routes.folders,
state = ImageGalleryPage.Files(Lce.Loading())
state = ImageGalleryPage.Files(Lce.Loading(), folder)
)
)
}
@ -78,7 +78,7 @@ data class ImageGalleryState(
sealed interface ImageGalleryPage {
data class Folders(val content: Lce<List<Folder>>) : ImageGalleryPage
data class Files(val content: Lce<List<Media>>) : ImageGalleryPage
data class Files(val content: Lce<List<Media>>, val folder: Folder) : ImageGalleryPage
object Routes {
val folders = Route<Folders>("Folders")

View File

@ -119,7 +119,7 @@ private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile:
}
@Composable
private fun Invitations(viewModel: ProfileViewModel, invitations: Page.Invitations) {
private fun SpiderItemScope.Invitations(viewModel: ProfileViewModel, invitations: Page.Invitations) {
when (val state = invitations.content) {
is Lce.Loading -> CenteredLoading()
is Lce.Content -> {
@ -147,7 +147,7 @@ private fun Invitations(viewModel: ProfileViewModel, invitations: Page.Invitatio
}
}
is Lce.Error -> TODO()
is Lce.Error -> GenericError(label = "Go back", cause = state.cause) { goBack() }
}
}

View File

@ -31,7 +31,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -40,6 +39,7 @@ import app.dapk.st.core.Lce
import app.dapk.st.core.StartObserving
import app.dapk.st.core.components.CenteredLoading
import app.dapk.st.core.components.Header
import app.dapk.st.core.extensions.takeAs
import app.dapk.st.core.getActivity
import app.dapk.st.design.components.*
import app.dapk.st.matrix.crypto.ImportResult
@ -63,7 +63,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit,
}
Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) {
item(Page.Routes.root) {
RootSettings(it) { viewModel.onClick(it) }
RootSettings(it, onClick = { viewModel.onClick(it) }, onRetry = { viewModel.start() })
}
item(Page.Routes.encryption) {
Encryption(viewModel, it)
@ -149,21 +149,19 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit,
}
is ImportResult.Error -> {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val message = when (val type = result.cause) {
ImportResult.Error.Type.NoKeysFound -> "No keys found in the file"
ImportResult.Error.Type.UnexpectedDecryptionOutput -> "Unable to decrypt file, double check your passphrase"
is ImportResult.Error.Type.Unknown -> "${type.cause::class.java.simpleName}: ${type.cause.message}"
ImportResult.Error.Type.UnableToOpenFile -> "Unable to open file"
}
Text(text = "Import failed\n$message", textAlign = TextAlign.Center)
Spacer(modifier = Modifier.height(12.dp))
Button(onClick = { navigator.navigate.upToHome() }) {
Text(text = "Close".uppercase())
}
}
val message = when (result.cause) {
ImportResult.Error.Type.NoKeysFound -> "No keys found in the file"
ImportResult.Error.Type.UnexpectedDecryptionOutput -> "Unable to decrypt file, double check your passphrase"
is ImportResult.Error.Type.Unknown -> "Unknown error"
ImportResult.Error.Type.UnableToOpenFile -> "Unable to open file"
ImportResult.Error.Type.InvalidFile -> "Unable to process file"
}
GenericError(
message = message,
label = "Close",
cause = result.cause.takeAs<ImportResult.Error.Type.Unknown>()?.cause
) {
navigator.navigate.upToHome()
}
}
@ -182,7 +180,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit,
}
@Composable
private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) {
private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit, onRetry: () -> Unit) {
when (val content = page.content) {
is Lce.Content -> {
LazyColumn(
@ -228,12 +226,10 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) {
}
}
is Lce.Error -> {
// TODO
}
is Lce.Error -> GenericError(cause = content.cause, action = onRetry)
is Lce.Loading -> {
// TODO
// Should be quick enough to avoid needing a loading state
}
}
}
@ -266,7 +262,7 @@ private fun PushProviders(viewModel: SettingsViewModel, state: Page.PushProvider
}
}
is Lce.Error -> TODO()
is Lce.Error -> GenericError(cause = lce.cause) { viewModel.fetchPushProviders() }
}
}

View File

@ -14,6 +14,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.dapk.st.core.AppLogTag
import app.dapk.st.core.Lce
import app.dapk.st.core.components.CenteredLoading
import app.dapk.st.design.components.GenericError
import app.dapk.st.matrix.common.MatrixLogTag
private val filterItems = listOf<String?>(null) + (MatrixLogTag.values().map { it.key } + AppLogTag.values().map { it.key }).distinct()
@ -33,11 +35,13 @@ fun EventLogScreen(viewModel: EventLoggerViewModel) {
viewModel.selectLog(it, filter = null)
}
}
else -> {
Events(
selectedPageContent = state.selectedState,
onExit = { viewModel.exitLog() },
onSelectTag = { viewModel.selectLog(state.selectedState.selectedPage, it) }
onSelectTag = { viewModel.selectLog(state.selectedState.selectedPage, it) },
onRetry = { viewModel.start() },
)
}
}
@ -46,6 +50,7 @@ fun EventLogScreen(viewModel: EventLoggerViewModel) {
is Lce.Error -> {
// TODO
}
is Lce.Loading -> {
// TODO
}
@ -69,7 +74,7 @@ private fun LogKeysList(keys: List<String>, onSelected: (String) -> Unit) {
}
@Composable
private fun Events(selectedPageContent: SelectedState, onExit: () -> Unit, onSelectTag: (String?) -> Unit) {
private fun Events(selectedPageContent: SelectedState, onExit: () -> Unit, onSelectTag: (String?) -> Unit, onRetry: () -> Unit) {
BackHandler(onBack = onExit)
when (val content = selectedPageContent.content) {
is Lce.Content -> {
@ -112,9 +117,8 @@ private fun Events(selectedPageContent: SelectedState, onExit: () -> Unit, onSel
}
}
}
is Lce.Error -> TODO()
is Lce.Loading -> {
// TODO
}
is Lce.Error -> GenericError(cause = content.cause, action = onRetry)
is Lce.Loading -> CenteredLoading()
}
}

View File

@ -184,6 +184,7 @@ sealed interface ImportResult {
object NoKeysFound : Type
object UnexpectedDecryptionOutput : Type
object UnableToOpenFile : Type
object InvalidFile : Type
}
}

View File

@ -86,21 +86,27 @@ class RoomKeyImporter(
val line = it.joinToString(separator = "").replace("\n", "")
val toByteArray = base64.decode(line)
if (index == 0) {
decryptCipher.initialize(toByteArray, password)
toByteArray
.copyOfRange(37, toByteArray.size)
.decrypt(decryptCipher)
.also {
if (!it.startsWith("[{")) {
throw ImportException(ImportResult.Error.Type.UnexpectedDecryptionOutput)
}
toByteArray.ensureHasCipherPayloadOrThrow()
val initializer = toByteArray.copyOfRange(0, 37)
decryptCipher.initialize(initializer, password)
val content = toByteArray.copyOfRange(37, toByteArray.size)
content.decrypt(decryptCipher).also {
if (!it.startsWith("[{")) {
throw ImportException(ImportResult.Error.Type.UnexpectedDecryptionOutput)
}
}
} else {
toByteArray.decrypt(decryptCipher)
}
}
}
private fun ByteArray.ensureHasCipherPayloadOrThrow() {
if (this.size < 37) {
throw ImportException(ImportResult.Error.Type.InvalidFile)
}
}
private fun Cipher.initialize(payload: ByteArray, passphrase: String) {
val salt = payload.copyOfRange(1, 1 + 16)
val iv = payload.copyOfRange(17, 17 + 16)
@ -176,6 +182,7 @@ private class JsonAccumulator {
jsonSegment = withLatest
null
}
else -> {
val string = withLatest.substring(objectRange)
importJson.decodeFromString(ElementMegolmExportObject.serializer(), string).also {
@ -200,6 +207,7 @@ private class JsonAccumulator {
}
opens++
}
c == '}' -> {
opens--
if (opens == 0) {