Much better error/badconfig handling
This commit is contained in:
@@ -5,6 +5,7 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import net.buzzert.kordophone.backend.db.CachedChatDatabase
|
import net.buzzert.kordophone.backend.db.CachedChatDatabase
|
||||||
|
import net.buzzert.kordophone.backend.server.APIClientFactory
|
||||||
import net.buzzert.kordophone.backend.server.Authentication
|
import net.buzzert.kordophone.backend.server.Authentication
|
||||||
import net.buzzert.kordophone.backend.server.ChatRepository
|
import net.buzzert.kordophone.backend.server.ChatRepository
|
||||||
import net.buzzert.kordophone.backend.server.RetrofitAPIClient
|
import net.buzzert.kordophone.backend.server.RetrofitAPIClient
|
||||||
@@ -19,18 +20,10 @@ object AppModule {
|
|||||||
@Provides
|
@Provides
|
||||||
fun provideChatRepository(configRepository: ServerConfigRepository): ChatRepository {
|
fun provideChatRepository(configRepository: ServerConfigRepository): ChatRepository {
|
||||||
val serverConfig = configRepository.serverConfig.value
|
val serverConfig = configRepository.serverConfig.value
|
||||||
val authentication = serverConfig.authentication?.let {
|
|
||||||
Authentication(it.username, it.password)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This is really bad error handling...
|
val server = serverConfig.serverName
|
||||||
val baseURL: URL = try { URL(serverConfig.serverName) }
|
val authentication = serverConfig.authentication?.toBackendAuthentication()
|
||||||
catch (e: java.net.MalformedURLException) { URL("http://localhost") }
|
val client = APIClientFactory.createClient(server, authentication)
|
||||||
|
|
||||||
val client = RetrofitAPIClient(
|
|
||||||
baseURL = baseURL,
|
|
||||||
authentication = authentication ?: Authentication("", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
val database = CachedChatDatabase.liveDatabase()
|
val database = CachedChatDatabase.liveDatabase()
|
||||||
return ChatRepository(client, database)
|
return ChatRepository(client, database)
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import net.buzzert.kordophone.backend.server.ChatRepository
|
|||||||
import net.buzzert.kordophonedroid.ui.messagelist.MVM_LOG
|
import net.buzzert.kordophonedroid.ui.messagelist.MVM_LOG
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
const val AVM_LOG: String = "AttachmentViewModel"
|
||||||
|
|
||||||
data class AttachmentFetchData(
|
data class AttachmentFetchData(
|
||||||
val guid: String,
|
val guid: String,
|
||||||
val preview: Boolean = false
|
val preview: Boolean = false
|
||||||
@@ -72,7 +74,7 @@ private class AttachmentFetcher(
|
|||||||
val data: AttachmentFetchData
|
val data: AttachmentFetchData
|
||||||
): Fetcher {
|
): Fetcher {
|
||||||
override suspend fun fetch(): FetchResult {
|
override suspend fun fetch(): FetchResult {
|
||||||
Log.d(MVM_LOG, "Loading attachment ${data.guid} from network")
|
Log.d(AVM_LOG, "Loading attachment ${data.guid} from network")
|
||||||
val source = repository.fetchAttachmentDataSource(data.guid, data.preview)
|
val source = repository.fetchAttachmentDataSource(data.guid, data.preview)
|
||||||
return SourceResult(
|
return SourceResult(
|
||||||
source = ImageSource(source, context),
|
source = ImageSource(source, context),
|
||||||
|
|||||||
@@ -10,10 +10,13 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import net.buzzert.kordophone.backend.model.Conversation
|
import net.buzzert.kordophone.backend.model.Conversation
|
||||||
|
import net.buzzert.kordophone.backend.server.APIClientFactory
|
||||||
import net.buzzert.kordophone.backend.server.Authentication
|
import net.buzzert.kordophone.backend.server.Authentication
|
||||||
import net.buzzert.kordophone.backend.server.ChatRepository
|
import net.buzzert.kordophone.backend.server.ChatRepository
|
||||||
import net.buzzert.kordophone.backend.server.RetrofitAPIClient
|
import net.buzzert.kordophone.backend.server.RetrofitAPIClient
|
||||||
import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository
|
import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -37,21 +40,11 @@ class ConversationListViewModel @Inject constructor(
|
|||||||
serverConfigRepository.serverConfig.collect { config ->
|
serverConfigRepository.serverConfig.collect { config ->
|
||||||
Log.d(CL_VM_LOG, "Got settings change.")
|
Log.d(CL_VM_LOG, "Got settings change.")
|
||||||
|
|
||||||
// Check settings
|
|
||||||
try {
|
|
||||||
val baseURL = URL(config.serverName)
|
|
||||||
val authentication = config.authentication?.let { serverAuth ->
|
|
||||||
Authentication(serverAuth.username, serverAuth.password)
|
|
||||||
} ?: throw Error("No authentication data.")
|
|
||||||
|
|
||||||
// Make new APIClient
|
// Make new APIClient
|
||||||
val apiClient = RetrofitAPIClient(baseURL, authentication)
|
val baseURL = config.serverName
|
||||||
|
val authentication = config.authentication?.toBackendAuthentication()
|
||||||
|
val apiClient = APIClientFactory.createClient(baseURL, authentication)
|
||||||
chatRepository.updateAPIClient(apiClient)
|
chatRepository.updateAPIClient(apiClient)
|
||||||
} catch (e: Error) {
|
|
||||||
Log.e(CL_VM_LOG, "Error re-creating API client for settings change: $e")
|
|
||||||
} catch (e: java.net.MalformedURLException) {
|
|
||||||
Log.e(CL_VM_LOG, "Malformed server URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform db synchronization
|
// Perform db synchronization
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.security.crypto.MasterKey
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import net.buzzert.kordophone.backend.server.Authentication
|
||||||
import java.lang.reflect.Constructor
|
import java.lang.reflect.Constructor
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -84,6 +85,10 @@ data class ServerAuthentication(
|
|||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toBackendAuthentication(): Authentication {
|
||||||
|
return Authentication(username, password)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package net.buzzert.kordophone.backend.server
|
package net.buzzert.kordophone.backend.server
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import net.buzzert.kordophone.backend.model.Conversation
|
||||||
|
import net.buzzert.kordophone.backend.model.GUID
|
||||||
|
import net.buzzert.kordophone.backend.model.Message
|
||||||
import okhttp3.Authenticator
|
import okhttp3.Authenticator
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody
|
||||||
import okhttp3.Route
|
import okhttp3.Route
|
||||||
import okhttp3.WebSocket
|
import okhttp3.WebSocket
|
||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
@@ -71,13 +75,15 @@ class TokenAuthenticator(
|
|||||||
apiInterface.authenticate(request).body()
|
apiInterface.authenticate(request).body()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token == null) {
|
when (token) {
|
||||||
|
null -> {
|
||||||
// Auth failure.
|
// Auth failure.
|
||||||
// TODO: How to bubble this up?
|
// TODO: How to bubble this up?
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update token store
|
// Update token store
|
||||||
|
else -> {
|
||||||
tokenStore.authenticationToken = token.serializedToken
|
tokenStore.authenticationToken = token.serializedToken
|
||||||
|
|
||||||
return response.request().newBuilder()
|
return response.request().newBuilder()
|
||||||
@@ -85,6 +91,57 @@ class TokenAuthenticator(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class APIClientFactory {
|
||||||
|
companion object {
|
||||||
|
fun createClient(serverString: String?, authentication: Authentication?): APIClient {
|
||||||
|
if (serverString == null || authentication == null) {
|
||||||
|
return InvalidConfigurationAPIClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse server string
|
||||||
|
val serverURL = HttpUrl.parse(serverString) ?: return InvalidConfigurationAPIClient()
|
||||||
|
|
||||||
|
return RetrofitAPIClient(serverURL.url(), authentication)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Is this a dumb idea?
|
||||||
|
class InvalidConfigurationAPIClient: APIClient {
|
||||||
|
private class InvalidConfigurationAPIInterface: APIInterface {
|
||||||
|
private fun throwError(): Nothing {
|
||||||
|
throw Error("Invalid Configuration.")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getVersion(): ResponseBody = throwError()
|
||||||
|
override suspend fun getConversations(): retrofit2.Response<List<Conversation>> = throwError()
|
||||||
|
override suspend fun sendMessage(request: SendMessageRequest): retrofit2.Response<SendMessageResponse> = throwError()
|
||||||
|
override suspend fun markConversation(conversationGUID: String): retrofit2.Response<Void> = throwError()
|
||||||
|
override suspend fun fetchAttachment(guid: String, preview: Boolean): ResponseBody = throwError()
|
||||||
|
override suspend fun authenticate(request: AuthenticationRequest): retrofit2.Response<AuthenticationResponse> = throwError()
|
||||||
|
override suspend fun getMessages(
|
||||||
|
conversationGUID: String,
|
||||||
|
limit: Int?,
|
||||||
|
beforeMessageGUID: GUID?,
|
||||||
|
afterMessageGUID: GUID?
|
||||||
|
): retrofit2.Response<List<Message>> = throwError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAPIInterface(): APIInterface {
|
||||||
|
return InvalidConfigurationAPIInterface()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getWebSocketClient(
|
||||||
|
serverPath: String,
|
||||||
|
queryParams: Map<String, String>?,
|
||||||
|
listener: WebSocketListener
|
||||||
|
): WebSocket {
|
||||||
|
throw Error("Invalid configuration.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class RetrofitAPIClient(
|
class RetrofitAPIClient(
|
||||||
private val baseURL: URL,
|
private val baseURL: URL,
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ class ChatRepository(
|
|||||||
open val title: String = "Error"
|
open val title: String = "Error"
|
||||||
open val description: String = "Generic Error"
|
open val description: String = "Generic Error"
|
||||||
|
|
||||||
data class ConnectionError(val exception: java.lang.Exception): Error() {
|
data class ConnectionError(val message: String?): Error() {
|
||||||
override val title: String = "Connection Error"
|
override val title: String = "Connection Error"
|
||||||
override val description: String = exception.message ?: "???"
|
override val description: String = message ?: "???"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +172,9 @@ class ChatRepository(
|
|||||||
synchronizeConversation(conversation)
|
synchronizeConversation(conversation)
|
||||||
}
|
}
|
||||||
} catch (e: java.lang.Exception) {
|
} catch (e: java.lang.Exception) {
|
||||||
_errorEncounteredChannel.emit(Error.ConnectionError(e))
|
_errorEncounteredChannel.emit(Error.ConnectionError(e.message))
|
||||||
|
} catch (e: java.lang.Error) {
|
||||||
|
_errorEncounteredChannel.emit(Error.ConnectionError(e.message))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun synchronizeConversation(conversation: Conversation, limit: Int = 15) = try {
|
suspend fun synchronizeConversation(conversation: Conversation, limit: Int = 15) = try {
|
||||||
@@ -181,7 +183,7 @@ class ChatRepository(
|
|||||||
val messages = fetchMessages(conversation, limit = limit)
|
val messages = fetchMessages(conversation, limit = limit)
|
||||||
database.writeMessages(messages, conversation)
|
database.writeMessages(messages, conversation)
|
||||||
} catch (e: java.lang.Exception) {
|
} catch (e: java.lang.Exception) {
|
||||||
_errorEncounteredChannel.emit(Error.ConnectionError(e))
|
_errorEncounteredChannel.emit(Error.ConnectionError(e.message))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun markConversationAsRead(conversation: Conversation) = try {
|
suspend fun markConversationAsRead(conversation: Conversation) = try {
|
||||||
|
|||||||
@@ -41,11 +41,15 @@ class UpdateMonitor(private val client: APIClient) : WebSocketListener() {
|
|||||||
|
|
||||||
fun beginMonitoringUpdates() {
|
fun beginMonitoringUpdates() {
|
||||||
Log.d(UPMON_LOG, "Opening websocket connection")
|
Log.d(UPMON_LOG, "Opening websocket connection")
|
||||||
|
try {
|
||||||
this.webSocket = client.getWebSocketClient(
|
this.webSocket = client.getWebSocketClient(
|
||||||
serverPath = "updates",
|
serverPath = "updates",
|
||||||
queryParams = mapOf("seq" to messageSeq.toString()),
|
queryParams = mapOf("seq" to messageSeq.toString()),
|
||||||
listener = this
|
listener = this
|
||||||
)
|
)
|
||||||
|
} catch (e: Error) {
|
||||||
|
Log.e(UPMON_LOG, "Error getting websocket client: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopMonitoringForUpdates() {
|
fun stopMonitoringForUpdates() {
|
||||||
|
|||||||
Reference in New Issue
Block a user