Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ dependencies {
implementation(libs.io.koin.core)
implementation(libs.io.koin.android)
implementation(libs.io.koin.compose)
implementation(libs.io.koin.androidx.compose)
implementation(libs.io.koin.test)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package dev.arkbuilders.drop.app.data
import dev.arkbuilders.drop.SendFilesConnectingEvent
import dev.arkbuilders.drop.SendFilesSendingEvent
import dev.arkbuilders.drop.SendFilesSubscriber
import dev.arkbuilders.drop.SendFilesToConnectingEvent
import dev.arkbuilders.drop.SendFilesToSendingEvent
import dev.arkbuilders.drop.SendFilesToSubscriber
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand Down Expand Up @@ -55,8 +58,39 @@ class SendFilesSubscriberImpl : SendFilesSubscriber {
receiverAvatar = event.receiver.avatarB64,
)
}
}

class SendFilesToSubscriberImpl : SendFilesToSubscriber {
private val id = UUID.randomUUID().toString()

private val _progress = MutableStateFlow(SendingProgress())
val progress: StateFlow<SendingProgress> = _progress.asStateFlow()

override fun getId() = id

override fun log(message: String) {
Timber.d(message)
}

override fun notifyConnecting(event: SendFilesToConnectingEvent) {
log("Connected to receiver: ${event.receiver.name}")

_progress.value =
_progress.value.copy(
isConnected = true,
receiverName = event.receiver.name,
receiverAvatar = event.receiver.avatarB64,
)
}

override fun notifySending(event: SendFilesToSendingEvent) {
log("Sending progress: ${event.name} - sent: ${event.sent}, remaining: ${event.remaining}")

fun reset() {
_progress.value = SendingProgress()
_progress.value =
_progress.value.copy(
fileName = event.name,
sent = event.sent,
remaining = event.remaining,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ class SenderFileDataImpl(
}
}

override fun isEmpty(): Boolean {
return false
}

override fun len(): ULong {
initialize()
return totalLength
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package dev.arkbuilders.drop.app.data.repository

import android.net.Uri
import dev.arkbuilders.drop.app.data.SendFilesSubscriberImpl
import dev.arkbuilders.drop.app.data.SendFilesToSubscriberImpl
import dev.arkbuilders.drop.app.domain.ResourcesHelper
import dev.arkbuilders.drop.app.domain.model.DropFileInfo
import dev.arkbuilders.drop.app.domain.model.ISendSession
import dev.arkbuilders.drop.app.domain.model.SendSession
import dev.arkbuilders.drop.app.domain.model.SendToSession
import dev.arkbuilders.drop.app.domain.model.TransferStatus
import dev.arkbuilders.drop.app.domain.repository.TransferSessionRepo
import dev.arkbuilders.drop.app.domain.usecase.SendFilesToUseCase
import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -19,15 +23,16 @@ import timber.log.Timber

class SendSessionRepo(
private val sendUseCase: SendFilesUseCase,
private val sendFilesToUseCase: SendFilesToUseCase,
private val resourcesHelper: ResourcesHelper,
private val transferSessionRepository: TransferSessionRepo,
) {
// Keep references to active sessions here so file transfers continue even if the ViewModel dies
private val activeSessions = mutableListOf<SendSession>()
private val activeSessions = mutableListOf<ISendSession>()
private val activeSessionsMutex = Mutex()
private val cancelScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

suspend fun sendFiles(fileUris: List<Uri>): SendSession? =
suspend fun sendFiles(fileUris: List<Uri>): ISendSession? =
withContext(Dispatchers.IO) {
cleanupFinishedSessions()

Expand All @@ -50,13 +55,42 @@ class SendSessionRepo(
)
}

suspend fun sendFilesTo(
ticket: String,
confirmation: UByte,
fileUris: List<Uri>,
): ISendSession? =
withContext(Dispatchers.IO) {
cleanupFinishedSessions()

sendFilesToUseCase.invoke(ticket, confirmation, fileUris).fold(
onSuccess = { bubble ->
val subscriber =
SendFilesToSubscriberImpl().also { subscriber ->
bubble.subscribe(subscriber)
}

val session = SendToSession(bubble, subscriber)
activeSessionsMutex.withLock {
activeSessions.add(session)
}

bubble.start()
return@withContext session
},
onFailure = {
return@withContext null
},
)
}

suspend fun recordSendCompletion(
fileUris: List<Uri>,
session: SendSession,
session: ISendSession,
) {
try {
cleanupFinishedSessions()
val progress = session.subscriber.progress.value
val progress = session.progress.value
val receiverName = progress.receiverName
val receiverAvatar = progress.receiverAvatar

Expand All @@ -79,14 +113,13 @@ class SendSessionRepo(
}
}

fun cancelSend(session: SendSession) {
fun cancelSend(session: ISendSession) {
cancelScope.launch {
try {
activeSessionsMutex.withLock {
activeSessions.remove(session)
}
session.bubble.unsubscribe(session.subscriber)
session.bubble.cancel()
session.cancel()
} catch (e: Throwable) {
Timber.e("Error cancelling send ${e.message}")
}
Expand All @@ -95,6 +128,6 @@ class SendSessionRepo(

private suspend fun cleanupFinishedSessions() =
activeSessionsMutex.withLock {
activeSessions.removeAll { it.bubble.isFinished() }
activeSessions.removeAll { it.isFinished() }
}
}
4 changes: 3 additions & 1 deletion app/src/main/java/dev/arkbuilders/drop/app/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import dev.arkbuilders.drop.app.domain.repository.NetworkStatus
import dev.arkbuilders.drop.app.domain.repository.ProfileRepo
import dev.arkbuilders.drop.app.domain.repository.TransferSessionRepo
import dev.arkbuilders.drop.app.domain.usecase.ReceiveFilesUseCase
import dev.arkbuilders.drop.app.domain.usecase.SendFilesToUseCase
import dev.arkbuilders.drop.app.domain.usecase.SendFilesUseCase
import org.koin.dsl.module

Expand All @@ -33,12 +34,13 @@ val appModule =
single<AvatarHelper> { AvatarHelperImpl(get()) }
single { ProfileLocalDataSource(get(), get()) }
single { TransferSessionLocalDataSource(get()) }
single { SendSessionRepo(get(), get(), get()) }
single { SendSessionRepo(get(), get(), get(), get()) }
single { ReceiveSessionRepo(get(), get(), get()) }
factory<TransferSessionDao> {
val db: Database = get()
db.transferHistoryDao()
}
factory<SendFilesUseCase> { SendFilesUseCase(get(), get(), get()) }
factory<ReceiveFilesUseCase> { ReceiveFilesUseCase(get()) }
factory { SendFilesToUseCase(get(), get(), get()) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dev.arkbuilders.drop.app.presentation.profile.EditProfileViewModel
import dev.arkbuilders.drop.app.presentation.receive.ReceiveViewModel
import dev.arkbuilders.drop.app.presentation.send.SendViewModel
import org.koin.core.module.dsl.viewModel
import org.koin.core.parameter.parametersOf
import org.koin.dsl.module

val viewModelsModule =
Expand All @@ -14,5 +15,12 @@ val viewModelsModule =
viewModel { HomeViewModel(get(), get(), get()) }
viewModel { EditProfileViewModel(get(), get()) }
viewModel { ReceiveViewModel(get(), get()) }
viewModel { SendViewModel(get(), get(), get()) }
viewModel { (isScanToSend: Boolean) ->
SendViewModel(
get { parametersOf(isScanToSend) },
get(),
get(),
get(),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package dev.arkbuilders.drop.app.domain.model

import dev.arkbuilders.drop.app.data.SendingProgress
import kotlinx.coroutines.flow.StateFlow

interface ISendSession {
fun isFinished(): Boolean

suspend fun cancel()

fun ticket(): String?

fun confirmation(): UByte?

val progress: StateFlow<SendingProgress>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,23 @@ package dev.arkbuilders.drop.app.domain.model

import dev.arkbuilders.drop.SendFilesBubble
import dev.arkbuilders.drop.app.data.SendFilesSubscriberImpl
import dev.arkbuilders.drop.app.data.SendingProgress
import kotlinx.coroutines.flow.StateFlow

class SendSession(
val bubble: SendFilesBubble,
val subscriber: SendFilesSubscriberImpl,
)
private val bubble: SendFilesBubble,
private val subscriber: SendFilesSubscriberImpl,
) : ISendSession {
override fun isFinished(): Boolean = bubble.isFinished()

override suspend fun cancel() {
bubble.cancel()
bubble.unsubscribe(subscriber)
}

override fun ticket(): String? = bubble.getTicket()

override fun confirmation(): UByte? = bubble.getConfirmation()

override val progress: StateFlow<SendingProgress> = subscriber.progress
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dev.arkbuilders.drop.app.domain.model

import dev.arkbuilders.drop.SendFilesToBubble
import dev.arkbuilders.drop.app.data.SendFilesToSubscriberImpl
import dev.arkbuilders.drop.app.data.SendingProgress
import kotlinx.coroutines.flow.StateFlow

class SendToSession(
private val bubble: SendFilesToBubble,
private val subscriber: SendFilesToSubscriberImpl,
) : ISendSession {
override fun isFinished(): Boolean = bubble.isFinished()

override suspend fun cancel() {
bubble.cancel()
bubble.unsubscribe(subscriber)
}

override fun ticket(): String? = null

override fun confirmation(): UByte? = null

override val progress: StateFlow<SendingProgress> = subscriber.progress
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package dev.arkbuilders.drop.app.domain.usecase

import dev.arkbuilders.drop.ReadyToReceiveRequest
import dev.arkbuilders.drop.ReceiverConfig
import dev.arkbuilders.drop.ReceiverProfile
import dev.arkbuilders.drop.app.domain.repository.ProfileRepo
import dev.arkbuilders.drop.readyToReceive
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import kotlin.text.ifEmpty

class ReadyToReceiveFilesUseCase(
private val profileRepo: ProfileRepo,
) {
suspend operator fun invoke() =
withContext(Dispatchers.IO) {
runCatching {
Timber.d("Starting file receive")

val profile = profileRepo.getCurrentProfile()
val receiverProfile =
ReceiverProfile(
name = profile.name.ifEmpty { "Anonymous" },
avatarB64 = profile.avatar.base64.takeIf { it.isNotEmpty() },
)

val request =
ReadyToReceiveRequest(
profile = receiverProfile,
config =
ReceiverConfig(
chunkSize = 1024u * 512u,
parallelStreams = 4u,
),
)

val bubble = readyToReceive(request)

Timber.d(
"Receive bubble created with ticket and confirmation: ${
bubble.getTicket()
} ${bubble.getConfirmation()}",
)
bubble
}.onFailure {
Timber.e("Error starting file receive ${it.message}")
}
}
}
Loading