More pleasant OOTB experience
This commit is contained in:
@@ -20,6 +20,8 @@ import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.net.URL
|
||||
|
||||
interface APIClient {
|
||||
val isConfigured: Boolean
|
||||
|
||||
fun getAPIInterface(): APIInterface
|
||||
fun getWebSocketClient(
|
||||
serverPath: String,
|
||||
@@ -99,11 +101,12 @@ class APIClientFactory {
|
||||
companion object {
|
||||
fun createClient(serverString: String?, authentication: Authentication?): APIClient {
|
||||
if (serverString == null || authentication == null) {
|
||||
return InvalidConfigurationAPIClient()
|
||||
return InvalidConfigurationAPIClient(InvalidConfigurationAPIClient.Issue.NOT_CONFIGURED)
|
||||
}
|
||||
|
||||
// Try to parse server string
|
||||
val serverURL = HttpUrl.parse(serverString) ?: return InvalidConfigurationAPIClient()
|
||||
val serverURL = HttpUrl.parse(serverString)
|
||||
?: return InvalidConfigurationAPIClient(InvalidConfigurationAPIClient.Issue.INVALID_HOST_URL)
|
||||
|
||||
return RetrofitAPIClient(serverURL.url(), authentication)
|
||||
}
|
||||
@@ -111,10 +114,23 @@ class APIClientFactory {
|
||||
}
|
||||
|
||||
// TODO: Is this a dumb idea?
|
||||
class InvalidConfigurationAPIClient: APIClient {
|
||||
private class InvalidConfigurationAPIInterface: APIInterface {
|
||||
class InvalidConfigurationAPIClient(val issue: Issue): APIClient {
|
||||
enum class Issue {
|
||||
NOT_CONFIGURED,
|
||||
INVALID_CONFIGURATION,
|
||||
INVALID_HOST_URL,
|
||||
}
|
||||
|
||||
class NotConfiguredError: Throwable(message = "Not configured.")
|
||||
class InvalidConfigurationError(submessage: String): Throwable(message = "Invalid configuration: $submessage")
|
||||
|
||||
private class InvalidConfigurationAPIInterface(val issue: Issue): APIInterface {
|
||||
private fun throwError(): Nothing {
|
||||
throw Error("Invalid Configuration.")
|
||||
when (issue) {
|
||||
Issue.NOT_CONFIGURED -> throw NotConfiguredError()
|
||||
Issue.INVALID_CONFIGURATION -> throw InvalidConfigurationError("Unknown.")
|
||||
Issue.INVALID_HOST_URL -> throw InvalidConfigurationError("Invalid host URL.")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getVersion(): ResponseBody = throwError()
|
||||
@@ -124,16 +140,14 @@ class InvalidConfigurationAPIClient: APIClient {
|
||||
override suspend fun fetchAttachment(guid: String, preview: Boolean): ResponseBody = throwError()
|
||||
override suspend fun uploadAttachment(filename: String, body: RequestBody): retrofit2.Response<UploadAttachmentResponse> = 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 suspend fun getMessages(conversationGUID: String, limit: Int?, beforeMessageGUID: GUID?, afterMessageGUID: GUID?): retrofit2.Response<List<Message>> = throwError()
|
||||
}
|
||||
|
||||
override val isConfigured: Boolean
|
||||
get() { return issue != Issue.NOT_CONFIGURED }
|
||||
|
||||
override fun getAPIInterface(): APIInterface {
|
||||
return InvalidConfigurationAPIInterface()
|
||||
return InvalidConfigurationAPIInterface(issue)
|
||||
}
|
||||
|
||||
override fun getWebSocketClient(
|
||||
@@ -162,6 +176,9 @@ class RetrofitAPIClient(
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
|
||||
override val isConfigured: Boolean
|
||||
get() = true
|
||||
|
||||
override fun getAPIInterface(): APIInterface {
|
||||
return retrofit.create(APIInterface::class.java)
|
||||
}
|
||||
|
||||
@@ -28,16 +28,12 @@ import java.io.InputStream
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ArrayBlockingQueue
|
||||
import kotlin.Boolean
|
||||
import kotlin.Int
|
||||
import kotlin.String
|
||||
import kotlin.let
|
||||
|
||||
const val REPO_LOG: String = "ChatRepository"
|
||||
const val CONVERSATION_MESSAGE_SYNC_COUNT = 10
|
||||
|
||||
class ChatRepository(
|
||||
apiClient: APIClient,
|
||||
private var apiClient: APIClient,
|
||||
private val database: CachedChatDatabase,
|
||||
) {
|
||||
sealed class Error {
|
||||
@@ -48,6 +44,11 @@ class ChatRepository(
|
||||
override val title: String = "Connection Error"
|
||||
override val description: String = message ?: "???"
|
||||
}
|
||||
|
||||
data class AttachmentError(val message: String): Error() {
|
||||
override val title: String = "Attachment Error"
|
||||
override val description: String = message
|
||||
}
|
||||
}
|
||||
|
||||
// All (Cached) Conversations
|
||||
@@ -71,6 +72,10 @@ class ChatRepository(
|
||||
val errorEncounteredChannel: SharedFlow<Error>
|
||||
get() = _errorEncounteredChannel.asSharedFlow()
|
||||
|
||||
val isConfigured: Boolean
|
||||
get() = apiClient.isConfigured
|
||||
|
||||
// New messages for a particular conversation
|
||||
fun messagesChanged(conversation: Conversation): Flow<List<Message>> =
|
||||
database.messagesChanged(conversation)
|
||||
|
||||
@@ -99,6 +104,7 @@ class ChatRepository(
|
||||
private var updateWatchScope: CoroutineScope? = null
|
||||
|
||||
fun updateAPIClient(client: APIClient) {
|
||||
this.apiClient = client
|
||||
this.apiInterface = client.getAPIInterface()
|
||||
this.updateMonitor = UpdateMonitor(client)
|
||||
|
||||
@@ -160,7 +166,7 @@ class ChatRepository(
|
||||
return database.fetchMessages(conversation)
|
||||
}
|
||||
|
||||
suspend fun synchronize() = try {
|
||||
suspend fun synchronize() = withErrorChannelHandling {
|
||||
Log.d(REPO_LOG, "Synchronizing conversations")
|
||||
|
||||
// Sync conversations
|
||||
@@ -173,26 +179,17 @@ class ChatRepository(
|
||||
for (conversation in sortedConversations.take(CONVERSATION_MESSAGE_SYNC_COUNT)) {
|
||||
synchronizeConversation(conversation)
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
_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) = withErrorChannelHandling {
|
||||
// TODO: Should only fetch messages after the last GUID we know about.
|
||||
// But keep in mind that outgoing message GUIDs are fake...
|
||||
val messages = fetchMessages(conversation, limit = limit)
|
||||
database.writeMessages(messages, conversation)
|
||||
} catch (e: java.lang.Exception) {
|
||||
_errorEncounteredChannel.emit(Error.ConnectionError(e.message))
|
||||
}
|
||||
|
||||
suspend fun markConversationAsRead(conversation: Conversation) = try {
|
||||
suspend fun markConversationAsRead(conversation: Conversation) = withErrorChannelHandling(silent = true) {
|
||||
apiInterface.markConversation(conversation.guid)
|
||||
} catch (e: java.lang.Exception) {
|
||||
// Don't report via the channel, but log it.
|
||||
Log.e(REPO_LOG, "Error marking conversation as read: ${e.message}")
|
||||
}
|
||||
|
||||
suspend fun fetchAttachmentDataSource(guid: String, preview: Boolean): BufferedSource {
|
||||
@@ -217,6 +214,18 @@ class ChatRepository(
|
||||
|
||||
// - private
|
||||
|
||||
private suspend fun withErrorChannelHandling(silent: Boolean = false, body: suspend () -> Unit) {
|
||||
try {
|
||||
body()
|
||||
} catch (e: InvalidConfigurationAPIClient.NotConfiguredError) {
|
||||
// Not configured yet: ignore.
|
||||
} catch (e: java.lang.Exception) {
|
||||
if (!silent) _errorEncounteredChannel.emit(Error.ConnectionError(e.message))
|
||||
} catch (e: java.lang.Error) {
|
||||
if (!silent) _errorEncounteredChannel.emit(Error.ConnectionError(e.message))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchConversations(): List<Conversation> {
|
||||
return apiInterface.getConversations().bodyOnSuccessOrThrow()
|
||||
}
|
||||
@@ -279,6 +288,7 @@ class ChatRepository(
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
Log.e(REPO_LOG, "Error uploading attachment (${e.message}). Dropping...")
|
||||
_errorEncounteredChannel.emit(Error.AttachmentError("Upload error: ${e.message}"))
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -40,6 +40,11 @@ class UpdateMonitor(private val client: APIClient) : WebSocketListener() {
|
||||
private val _messageAdded: MutableSharedFlow<Message> = MutableSharedFlow()
|
||||
|
||||
fun beginMonitoringUpdates() {
|
||||
if (!client.isConfigured) {
|
||||
Log.e(UPMON_LOG, "Closing websocket connection because client is not configured.")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(UPMON_LOG, "Opening websocket connection")
|
||||
try {
|
||||
this.webSocket = client.getWebSocketClient(
|
||||
|
||||
Reference in New Issue
Block a user