diff --git a/app/src/main/java/net/buzzert/kordophonedroid/AppModule.kt b/app/src/main/java/net/buzzert/kordophonedroid/AppModule.kt index 39c9a35..db19a1c 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/AppModule.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/AppModule.kt @@ -5,6 +5,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent 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.ChatRepository import net.buzzert.kordophone.backend.server.RetrofitAPIClient @@ -19,18 +20,10 @@ object AppModule { @Provides fun provideChatRepository(configRepository: ServerConfigRepository): ChatRepository { val serverConfig = configRepository.serverConfig.value - val authentication = serverConfig.authentication?.let { - Authentication(it.username, it.password) - } - // TODO: This is really bad error handling... - val baseURL: URL = try { URL(serverConfig.serverName) } - catch (e: java.net.MalformedURLException) { URL("http://localhost") } - - val client = RetrofitAPIClient( - baseURL = baseURL, - authentication = authentication ?: Authentication("", "") - ) + val server = serverConfig.serverName + val authentication = serverConfig.authentication?.toBackendAuthentication() + val client = APIClientFactory.createClient(server, authentication) val database = CachedChatDatabase.liveDatabase() return ChatRepository(client, database) diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/attachments/AttachmentViewModel.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/attachments/AttachmentViewModel.kt index 356c831..0ba86ac 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/attachments/AttachmentViewModel.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/attachments/AttachmentViewModel.kt @@ -20,6 +20,8 @@ import net.buzzert.kordophone.backend.server.ChatRepository import net.buzzert.kordophonedroid.ui.messagelist.MVM_LOG import javax.inject.Inject +const val AVM_LOG: String = "AttachmentViewModel" + data class AttachmentFetchData( val guid: String, val preview: Boolean = false @@ -72,7 +74,7 @@ private class AttachmentFetcher( val data: AttachmentFetchData ): Fetcher { 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) return SourceResult( source = ImageSource(source, context), diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListViewModel.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListViewModel.kt index fec359b..e4321c8 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListViewModel.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListViewModel.kt @@ -10,10 +10,13 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext 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.ChatRepository import net.buzzert.kordophone.backend.server.RetrofitAPIClient import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import java.net.URL import javax.inject.Inject @@ -37,21 +40,11 @@ class ConversationListViewModel @Inject constructor( serverConfigRepository.serverConfig.collect { config -> 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 - val apiClient = RetrofitAPIClient(baseURL, authentication) - 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") - } + // Make new APIClient + val baseURL = config.serverName + val authentication = config.authentication?.toBackendAuthentication() + val apiClient = APIClientFactory.createClient(baseURL, authentication) + chatRepository.updateAPIClient(apiClient) // Perform db synchronization withContext(Dispatchers.IO) { diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/shared/ServerConfigRepository.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/shared/ServerConfigRepository.kt index cd7fb2f..4a30883 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/shared/ServerConfigRepository.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/shared/ServerConfigRepository.kt @@ -8,6 +8,7 @@ import androidx.security.crypto.MasterKey import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import net.buzzert.kordophone.backend.server.Authentication import java.lang.reflect.Constructor import javax.inject.Inject import javax.inject.Singleton @@ -84,6 +85,10 @@ data class ServerAuthentication( apply() } } + + fun toBackendAuthentication(): Authentication { + return Authentication(username, password) + } } @Singleton diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/server/APIClient.kt b/backend/src/main/java/net/buzzert/kordophone/backend/server/APIClient.kt index 56b9e48..c36fa8f 100644 --- a/backend/src/main/java/net/buzzert/kordophone/backend/server/APIClient.kt +++ b/backend/src/main/java/net/buzzert/kordophone/backend/server/APIClient.kt @@ -1,12 +1,16 @@ package net.buzzert.kordophone.backend.server 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.HttpUrl import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response +import okhttp3.ResponseBody import okhttp3.Route import okhttp3.WebSocket import okhttp3.WebSocketListener @@ -71,18 +75,71 @@ class TokenAuthenticator( apiInterface.authenticate(request).body() } - if (token == null) { - // Auth failure. - // TODO: How to bubble this up? - return null + when (token) { + null -> { + // Auth failure. + // TODO: How to bubble this up? + return null + } + + // Update token store + else -> { + tokenStore.authenticationToken = token.serializedToken + + return response.request().newBuilder() + .header("Authorization", "Bearer ${token.serializedToken}") + .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.") } - // Update token store - tokenStore.authenticationToken = token.serializedToken + override suspend fun getVersion(): ResponseBody = throwError() + override suspend fun getConversations(): retrofit2.Response> = throwError() + override suspend fun sendMessage(request: SendMessageRequest): retrofit2.Response = throwError() + override suspend fun markConversation(conversationGUID: String): retrofit2.Response = throwError() + override suspend fun fetchAttachment(guid: String, preview: Boolean): ResponseBody = throwError() + override suspend fun authenticate(request: AuthenticationRequest): retrofit2.Response = throwError() + override suspend fun getMessages( + conversationGUID: String, + limit: Int?, + beforeMessageGUID: GUID?, + afterMessageGUID: GUID? + ): retrofit2.Response> = throwError() + } - return response.request().newBuilder() - .header("Authorization", "Bearer ${token.serializedToken}") - .build() + override fun getAPIInterface(): APIInterface { + return InvalidConfigurationAPIInterface() + } + + override fun getWebSocketClient( + serverPath: String, + queryParams: Map?, + listener: WebSocketListener + ): WebSocket { + throw Error("Invalid configuration.") } } diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt b/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt index c47a271..35b609b 100644 --- a/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt +++ b/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt @@ -41,9 +41,9 @@ class ChatRepository( open val title: String = "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 description: String = exception.message ?: "???" + override val description: String = message ?: "???" } } @@ -172,7 +172,9 @@ class ChatRepository( synchronizeConversation(conversation) } } 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 { @@ -181,7 +183,7 @@ class ChatRepository( val messages = fetchMessages(conversation, limit = limit) database.writeMessages(messages, conversation) } catch (e: java.lang.Exception) { - _errorEncounteredChannel.emit(Error.ConnectionError(e)) + _errorEncounteredChannel.emit(Error.ConnectionError(e.message)) } suspend fun markConversationAsRead(conversation: Conversation) = try { diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/server/UpdateMonitor.kt b/backend/src/main/java/net/buzzert/kordophone/backend/server/UpdateMonitor.kt index 33af3a9..220a042 100644 --- a/backend/src/main/java/net/buzzert/kordophone/backend/server/UpdateMonitor.kt +++ b/backend/src/main/java/net/buzzert/kordophone/backend/server/UpdateMonitor.kt @@ -41,11 +41,15 @@ class UpdateMonitor(private val client: APIClient) : WebSocketListener() { fun beginMonitoringUpdates() { Log.d(UPMON_LOG, "Opening websocket connection") - this.webSocket = client.getWebSocketClient( - serverPath = "updates", - queryParams = mapOf("seq" to messageSeq.toString()), - listener = this - ) + try { + this.webSocket = client.getWebSocketClient( + serverPath = "updates", + queryParams = mapOf("seq" to messageSeq.toString()), + listener = this + ) + } catch (e: Error) { + Log.e(UPMON_LOG, "Error getting websocket client: ${e.message}") + } } fun stopMonitoringForUpdates() {