[go: nahoru, domu]

Skip to content

Commit

Permalink
Refactor to use typed navigation arguments, common result handling
Browse files Browse the repository at this point in the history
  • Loading branch information
oblakr24 committed May 20, 2024
1 parent 60828f3 commit ebca08b
Show file tree
Hide file tree
Showing 27 changed files with 386 additions and 320 deletions.
2 changes: 1 addition & 1 deletion .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 12 additions & 11 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,22 +59,23 @@ android {

dependencies {
val composeHiltNavigationVersion = "1.2.0"
val navVersion = "2.7.7"
val composeUiVersion = "1.6.3"
val navVersion = "2.8.0-beta01"
val composeUiVersion = "1.6.7"
val timberVersion = "5.0.1"

// Core/activity/lifecycle
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.activity:activity-compose:1.8.2")
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.activity:activity-compose:1.9.0")

// Hilt
implementation("com.google.dagger:hilt-android:2.49")
kapt("com.google.dagger:hilt-compiler:2.49")
val hiltVersion = "2.51.1"
implementation("com.google.dagger:hilt-android:$hiltVersion")
kapt("com.google.dagger:hilt-compiler:$hiltVersion")

// Compose
implementation("androidx.compose.ui:ui:$composeUiVersion")
implementation("androidx.compose.ui:ui-tooling-preview:$composeUiVersion")
implementation("androidx.compose.material:material:1.6.3")
implementation("androidx.compose.material:material:1.6.7")
// Compose constraint layout
implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
// Compose tooling
Expand All @@ -86,9 +87,9 @@ dependencies {
// Compose permissions
implementation("com.google.accompanist:accompanist-permissions:0.35.0-alpha")
// Compose extended material icons
implementation("androidx.compose.material:material-icons-extended:1.6.3")
implementation("androidx.compose.material:material-icons-extended:1.6.7")
// Compose Material 3
implementation(platform("androidx.compose:compose-bom:2024.02.02"))
implementation(platform("androidx.compose:compose-bom:2024.05.00"))
implementation("androidx.compose.material3:material3")
// Coil
implementation("io.coil-kt:coil-compose:2.6.0")
Expand All @@ -97,13 +98,13 @@ dependencies {
implementation("com.jakewharton.timber:timber:$timberVersion")

// Datastore
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.datastore:datastore-preferences:1.1.1")

// KotlinX immutable collections
api("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")

// KotlinX Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")

// SMS-MMS parsing lib, used for MMS
implementation("com.klinkerapps:android-smsmms:5.2.6")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,33 @@ package com.rokoblak.chatbackup.data.model

interface RootError

sealed interface OperationResult<out T : Any?, out E: RootError> {
data class Done<out T : Any, out E: RootError>(val data: T) : OperationResult<T, E>
data class Error<out E: RootError>(val error: E) : OperationResult<Nothing, E>
sealed interface OperationResult<out T : Any?, out E : RootError> {
data class Done<out T : Any, out E : RootError>(val data: T) : OperationResult<T, E>
data class Error<out E : RootError>(val error: E) : OperationResult<Nothing, E>

fun <R : Any> map(mapper: (T) -> R): OperationResult<R, E> = when (this) {
is Done -> Done(mapper(data))
is Error -> this
}

fun optValue() = when (this) {
is Done -> data
is Error -> null
}

fun doOnError(block: (error: Error<E>) -> Unit): OperationResult<T, E> {
when (this) {
is Done -> Unit
is Error -> block(this)
}
return this
}

fun doOnSuccess(block: (data: T) -> Unit): OperationResult<T, E> {
when (this) {
is Done -> block(data)
is Error -> Unit
}
return this
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ class AppEventsUseCase @Inject constructor() {
}

sealed interface SMSEvent {
object NewReceived: SMSEvent
data object NewReceived: SMSEvent
data class OpenCreateChat(val address: String?): SMSEvent
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import com.rokoblak.chatbackup.data.model.Conversation
import com.rokoblak.chatbackup.data.model.Conversations
import com.rokoblak.chatbackup.data.model.Message
import com.rokoblak.chatbackup.data.model.MinimalContact
import com.rokoblak.chatbackup.data.model.OperationResult
import com.rokoblak.chatbackup.data.model.RootError
import com.rokoblak.chatbackup.data.util.JsonSerializer
import com.rokoblak.chatbackup.di.AppScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -21,44 +23,45 @@ class ConversationsImportUseCase @Inject constructor(
private val serializer: JsonSerializer,
) {

suspend fun importJson(uri: Uri): ImportResult = withContext(Dispatchers.IO) {
val cr = appScope.appContext.contentResolver
val inputStream = cr.openInputStream(uri)
?: return@withContext ImportResult.Error("Failed to open input stream")
suspend fun importJson(uri: Uri): OperationResult<ImportResult, ImportError> =
withContext(Dispatchers.IO) {
val cr = appScope.appContext.contentResolver
val inputStream = cr.openInputStream(uri)
?: return@withContext OperationResult.Error(ImportError.FailedToOpenStream)

val filename = queryName(uri)
val filename = queryName(uri)

val parsed = serializer.decodeStream(ConversationsDTO.serializer(), inputStream)
val parsed = serializer.decodeStream(ConversationsDTO.serializer(), inputStream)

val conversations = parsed.conversations.map { conv ->
val contact = Contact(
name = conv.contactName,
orgNumber = conv.contactNumber,
)
val messages = conv.messages.map { msg ->
val body = msg.content
val timestampMs = msg.timestampMs
val msgId = contact.id + body.hashCode() + timestampMs
Message(
id = msgId,
content = body,
contact = MinimalContact(conv.contactNumber),
timestamp = Instant.ofEpochMilli(timestampMs),
incoming = msg.incoming,
imageUri = null,
val conversations = parsed.conversations.map { conv ->
val contact = Contact(
name = conv.contactName,
orgNumber = conv.contactNumber,
)
val messages = conv.messages.map { msg ->
val body = msg.content
val timestampMs = msg.timestampMs
val msgId = contact.id + body.hashCode() + timestampMs
Message(
id = msgId,
content = body,
contact = MinimalContact(conv.contactNumber),
timestamp = Instant.ofEpochMilli(timestampMs),
incoming = msg.incoming,
imageUri = null,
)
}
Conversation(contact, messages = messages)
}
Conversation(contact, messages = messages)
}

val sortedConversations = conversations.sortedBy { it.messages.maxOf { it.timestamp } }
val contacts = sortedConversations.map { it.contact }.distinctBy { it.id }
val mapping = sortedConversations.associateBy {
it.contact
val sortedConversations = conversations.sortedBy { it.messages.maxOf { it.timestamp } }
val contacts = sortedConversations.map { it.contact }.distinctBy { it.id }
val mapping = sortedConversations.associateBy {
it.contact
}
val convs = Conversations(mapping = mapping, sortedContactsByLastMsg = contacts)
OperationResult.Done(ImportResult(filename, convs))
}
val convs = Conversations(mapping = mapping, sortedContactsByLastMsg = contacts)
ImportResult.Success(filename, convs)
}

private fun queryName(uri: Uri): String {
val returnCursor: Cursor =
Expand All @@ -71,7 +74,8 @@ class ConversationsImportUseCase @Inject constructor(
}
}

sealed interface ImportResult {
data class Error(val message: String) : ImportResult
data class Success(val filename: String, val convs: Conversations) : ImportResult
}
sealed interface ImportError : RootError {
data object FailedToOpenStream : ImportError
}

data class ImportResult(val filename: String, val convs: Conversations)
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ data class EditState(
)

sealed interface ConvsState {
object Empty : ConvsState
object Loading : ConvsState
data object Empty : ConvsState
data object Loading : ConvsState
data class Loaded(
val convs: Conversations,
val selections: Map<String, Boolean>?,
Expand All @@ -125,8 +125,8 @@ sealed interface ConvsState {
}

sealed interface SearchState {
object NoSearch : SearchState
object Searching : SearchState
object NoResults : SearchState
data object NoSearch : SearchState
data object Searching : SearchState
data object NoResults : SearchState
data class ResultsFound(val results: SearchResults) : SearchState
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class DownloadConversationUseCase @Inject constructor(

private val downloading = MutableStateFlow<DownloadProgress?>(null)
private val selections = MutableStateFlow(emptyMap<String, Boolean>())
private val importedConvs = MutableStateFlow<ImportResult?>(null)
private val importedConvs = MutableStateFlow<OperationResult<ImportResult, ImportError>?>(null)

val state = combine(downloading, selections, importedConvs) { progress, selections, importRes ->
ImportDownloadState(progress, selections, importRes)
Expand All @@ -47,28 +47,27 @@ class DownloadConversationUseCase @Inject constructor(
}
}

suspend fun importFile(doImport: suspend () -> ImportResult) {
suspend fun importFile(doImport: suspend () -> OperationResult<ImportResult, ImportError>) {
importedConvs.value = doImport().also { res ->
if (res is ImportResult.Success) {
selections.update { res.convs.mapping.map { it.key.id to true }.toMap() }
repo.setImportedConversations(res.convs)
}
val result = res.optValue() ?: return
selections.update { result.convs.mapping.map { it.key.id to true }.toMap() }
repo.setImportedConversations(result.convs)
}
}

fun deleteSelected() {
val res = importedConvs.value as? ImportResult.Success ?: return
val res = importedConvs.value?.optValue() ?: return
val convs = res.convs
val selected = selections.value
val keys = selected.filter { it.value }.keys
val removed = convs.removeConvs(keys)
repo.setImportedConversations(removed)
selections.update { it.toMutableMap().filterKeys { k -> keys.contains(k).not() } }
importedConvs.value = res.copy(convs = removed)
importedConvs.value = OperationResult.Done(res.copy(convs = removed))
}

suspend fun downloadSelected(onProgressMsg: (String) -> Unit) = withContext(Dispatchers.IO) {
val res = importedConvs.value as? ImportResult.Success ?: return@withContext
val res = importedConvs.value?.optValue() ?: return@withContext
val convs = res.convs
val selected = selections.value
val selectedMsgs = convs.retrieveMessages(selected.filter { it.value }.keys)
Expand Down Expand Up @@ -113,7 +112,7 @@ class DownloadConversationUseCase @Inject constructor(
data class ImportDownloadState(
val progress: DownloadProgress?,
val selections: Map<String, Boolean>,
val importResult: ImportResult?,
val importResult: OperationResult<ImportResult, ImportError>?,
)

data class DownloadProgress(val done: Int, val total: Int)
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,16 @@ class EventsNavigationUseCase @Inject constructor(
is SMSEvent.OpenCreateChat -> {
eventsUseCase.markEventConsumed()
if (event.address != null) {
val input = ConversationRoute.Input(
val input = ConversationRoute(
resolvedContactId = null,
address = event.address,
isImport = false
)
routeNavigator.navigateToRoute(ConversationRoute.get(input))
routeNavigator.navigateToRoute(input)
} else {
routeNavigator.navigateToRoute(CreateChatRoute.route)
routeNavigator.navigateToRoute(CreateChatRoute)
}
}
}
}.collect()

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package com.rokoblak.chatbackup.domain.usecases

import android.net.Uri
import com.rokoblak.chatbackup.data.model.Conversation
import com.rokoblak.chatbackup.data.util.JsonSerializer
import com.rokoblak.chatbackup.data.util.FileManager
import com.rokoblak.chatbackup.data.util.JsonSerializer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
Expand All @@ -13,34 +13,29 @@ class MessagesExportUseCase @Inject constructor(
private val fileManager: FileManager
) {

sealed interface ExportResult {
data class Success(val uri: Uri): ExportResult
data class Error(val throwable: Throwable): ExportResult
}

suspend fun serialize(sortedConversations: List<Conversation>): ExportResult = withContext(Dispatchers.IO) {
val convDtos = sortedConversations.map { conversation ->
val messageDtos = conversation.messages.map {
MessageDTO(
content = it.content,
timestampMs = it.timestamp.toEpochMilli(),
incoming = it.incoming
suspend fun serialize(sortedConversations: List<Conversation>): Uri =
withContext(Dispatchers.IO) {
val convDtos = sortedConversations.map { conversation ->
val messageDtos = conversation.messages.map {
MessageDTO(
content = it.content,
timestampMs = it.timestamp.toEpochMilli(),
incoming = it.incoming
)
}

SingleConversationDTO(
contactName = conversation.contact.name,
contactNumber = conversation.contact.number,
messages = messageDtos
)
}
val dto = ConversationsDTO(convDtos)

SingleConversationDTO(
contactName = conversation.contact.name,
contactNumber = conversation.contact.number,
messages = messageDtos
)
}
val dto = ConversationsDTO(convDtos)
val encoded = serializer.encode(ConversationsDTO.serializer(), dto)

val encoded = serializer.encode(ConversationsDTO.serializer(), dto)

val uri = fileManager.createNewJson(encoded)
ExportResult.Success(uri)
}
fileManager.createNewJson(encoded)
}
}

@kotlinx.serialization.Serializable
Expand Down
Loading

0 comments on commit ebca08b

Please sign in to comment.