Skip to content

Commit

Permalink
Merge pull request #151 from Infomaniak/upload-worker-ui
Browse files Browse the repository at this point in the history
feat: Track upload progress
  • Loading branch information
sirambd authored Nov 7, 2024
2 parents f517c24 + b58fa35 commit f0b6969
Show file tree
Hide file tree
Showing 14 changed files with 238 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ package com.infomaniak.swisstransfer.ui
import android.app.Application
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.infomaniak.multiplatform_swisstransfer.SwissTransferInjection
import com.infomaniak.multiplatform_swisstransfer.managers.AccountManager
import com.infomaniak.swisstransfer.BuildConfig
import com.infomaniak.swisstransfer.ui.utils.AccountUtils
import com.infomaniak.swisstransfer.ui.utils.UploadRecaptcha
Expand All @@ -37,7 +37,7 @@ import javax.inject.Inject
class MainApplication : Application(), Configuration.Provider {

@Inject
lateinit var swissTransferInjection: SwissTransferInjection
lateinit var accountManager: AccountManager

@Inject
lateinit var uploadRecaptcha: UploadRecaptcha
Expand All @@ -55,7 +55,7 @@ class MainApplication : Application(), Configuration.Provider {
super.onCreate()

globalCoroutineScope.launch {
AccountUtils.init(swissTransferInjection)
AccountUtils.init(accountManager)
uploadRecaptcha.initializeClient()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.infomaniak.swisstransfer.ui.navigation

import android.os.Bundle
import androidx.navigation.NavBackStackEntry
import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.components.TransferType
import kotlinx.serialization.Serializable
import kotlin.reflect.KClass
import kotlin.reflect.full.primaryConstructor
Expand Down Expand Up @@ -61,9 +62,9 @@ sealed class NewTransferNavigation : NavigationDestination() {
data object ValidateUserEmailDestination : NewTransferNavigation()

@Serializable
data object UploadProgressDestination : NewTransferNavigation()
data class UploadProgressDestination(val transferType: TransferType, val totalSize: Long) : NewTransferNavigation()
@Serializable
data object UploadSuccessDestination : NewTransferNavigation()
data class UploadSuccessDestination(val transferType: TransferType, val transferLink: String) : NewTransferNavigation()

companion object {
val startDestination = ImportFilesDestination
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
import com.infomaniak.swisstransfer.ui.navigation.NewTransferNavigation
import com.infomaniak.swisstransfer.ui.navigation.NewTransferNavigation.*
import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.ImportFilesScreen
import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.TransferOptionsScreen
import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.ValidateUserEmailScreen
import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.components.TransferType
import com.infomaniak.swisstransfer.ui.screen.newtransfer.upload.UploadProgressScreen
import com.infomaniak.swisstransfer.ui.screen.newtransfer.upload.UploadSuccessScreen

Expand All @@ -37,7 +37,9 @@ fun NewTransferNavHost(navController: NavHostController, closeActivity: () -> Un
composable<ImportFilesDestination> {
ImportFilesScreen(
closeActivity = closeActivity,
navigateToUploadProgress = { navController.navigate(UploadProgressDestination) }
navigateToUploadProgress = { transferType, totalSize ->
navController.navigate(UploadProgressDestination(transferType, totalSize))
},
)
}
composable<TransferOptionsDestination> {
Expand All @@ -47,11 +49,22 @@ fun NewTransferNavHost(navController: NavHostController, closeActivity: () -> Un
ValidateUserEmailScreen()
}
composable<UploadProgressDestination> {
UploadProgressScreen(closeActivity = closeActivity)
val args = it.toRoute<UploadProgressDestination>()
UploadProgressScreen(
totalSizeInBytes = args.totalSize,
navigateToUploadSuccess = { transferLink ->
navController.navigate(UploadSuccessDestination(args.transferType, transferLink))
},
closeActivity = closeActivity
)
}
composable<UploadSuccessDestination> {
// TODO: Use correct TransferType instead of hard-coded value
UploadSuccessScreen(transferType = TransferType.MAIL)
val args = it.toRoute<UploadSuccessDestination>()
UploadSuccessScreen(
transferType = args.transferType,
transferLink = args.transferLink,
closeActivity = closeActivity
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class NewTransferViewModel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {

private val _sendActionResult = MutableStateFlow<SendActionResult?>(null)
val sendActionResult = _sendActionResult.asStateFlow()

@OptIn(FlowPreview::class)
val importedFilesDebounced = importationFilesManager.importedFiles
.debounce(50)
Expand Down Expand Up @@ -96,14 +99,22 @@ class NewTransferViewModel @Inject constructor(
}
}

fun selectTransferType(type: TransferType) {
_selectedTransferType.value = type
}

fun sendTransfer() {
viewModelScope.launch(ioDispatcher) {
runCatching {
uploadManager.createAndGetUpload(generateNewUploadSession())
uploadWorkerScheduler.scheduleWork()
val uuid = uploadManager.createAndGetUpload(generateNewUploadSession()).uuid
uploadWorkerScheduler.scheduleWork(uuid)
_sendActionResult.update {
val totalSize = importationFilesManager.importedFiles.value.sumOf { it.fileSize }
SendActionResult.Success(totalSize)
}
}.onFailure { exception ->
// TODO: Handle user feedback
SentryLog.e(TAG, "Failed to start the upload", exception)
_sendActionResult.update { SendActionResult.Failure }
}
}
}
Expand Down Expand Up @@ -134,7 +145,8 @@ class NewTransferViewModel @Inject constructor(
private const val IS_VIEW_MODEL_RESTORED_KEY = "IS_VIEW_MODEL_RESTORED_KEY"
}

fun selectTransferType(type: TransferType) {
_selectedTransferType.value = type
sealed class SendActionResult {
data class Success(val totalSize: Long) : SendActionResult()
data object Failure : SendActionResult()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import com.infomaniak.swisstransfer.ui.screen.main.settings.DownloadLimitOption
import com.infomaniak.swisstransfer.ui.screen.main.settings.EmailLanguageOption
import com.infomaniak.swisstransfer.ui.screen.main.settings.ValidityPeriodOption
import com.infomaniak.swisstransfer.ui.screen.newtransfer.NewTransferViewModel
import com.infomaniak.swisstransfer.ui.screen.newtransfer.NewTransferViewModel.SendActionResult
import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.components.ImportedFilesCard
import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.components.TransferAdvancedSettings
import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.components.TransferType
Expand All @@ -58,12 +59,15 @@ private const val TOTAL_FILE_SIZE: Long = 50_000_000_000L
fun ImportFilesScreen(
newTransferViewModel: NewTransferViewModel = hiltViewModel<NewTransferViewModel>(),
closeActivity: () -> Unit,
navigateToUploadProgress: () -> Unit,
navigateToUploadProgress: (transferType: TransferType, totalSize: Long) -> Unit,
) {
val files by newTransferViewModel.importedFilesDebounced.collectAsStateWithLifecycle()
val filesToImportCount by newTransferViewModel.filesToImportCount.collectAsStateWithLifecycle()
val currentSessionFilesCount by newTransferViewModel.currentSessionFilesCount.collectAsStateWithLifecycle()
val selectedTransferType by newTransferViewModel.selectedTransferType.collectAsStateWithLifecycle()
val sendActionResult by newTransferViewModel.sendActionResult.collectAsStateWithLifecycle()

HandleSendActionResult({ sendActionResult }, { selectedTransferType }, navigateToUploadProgress)

ImportFilesScreen(
files = { files },
Expand All @@ -76,14 +80,26 @@ fun ImportFilesScreen(
removeFileByUid = newTransferViewModel::removeFileByUid,
addFiles = newTransferViewModel::importFiles,
closeActivity = closeActivity,
navigateToUploadProgress = {
newTransferViewModel.sendTransfer()
navigateToUploadProgress()
},
sendTransfer = newTransferViewModel::sendTransfer,
initialShowUploadSourceChoiceBottomSheet = true,
)
}

@Composable
private fun HandleSendActionResult(
getSendActionResult: () -> SendActionResult?,
transferType: () -> TransferType,
navigateToUploadProgress: (transferType: TransferType, totalSize: Long) -> Unit,
) {
LaunchedEffect(getSendActionResult()) {
when (val actionResult = getSendActionResult()) {
is SendActionResult.Success -> navigateToUploadProgress(transferType(), actionResult.totalSize)
is SendActionResult.Failure -> Unit //TODO: Show error
else -> Unit
}
}
}

@Composable
private fun ImportFilesScreen(
files: () -> List<FileUi>,
Expand All @@ -94,7 +110,7 @@ private fun ImportFilesScreen(
addFiles: (List<Uri>) -> Unit,
closeActivity: () -> Unit,
initialShowUploadSourceChoiceBottomSheet: Boolean,
navigateToUploadProgress: () -> Unit,
sendTransfer: () -> Unit,
) {
val context = LocalContext.current
var showUploadSourceChoiceBottomSheet by rememberSaveable { mutableStateOf(initialShowUploadSourceChoiceBottomSheet) }
Expand All @@ -121,7 +137,7 @@ private fun ImportFilesScreen(
)
},
topButton = { modifier ->
SendButton(filesToImportCount, currentSessionFilesCount, files, modifier, navigateToUploadProgress)
SendButton(filesToImportCount, currentSessionFilesCount, files, modifier, sendTransfer)
},
content = {
Column(
Expand Down Expand Up @@ -258,7 +274,7 @@ private fun ImportFilesScreenPreview(@PreviewParameter(FileUiListPreviewParamete
addFiles = {},
closeActivity = {},
initialShowUploadSourceChoiceBottomSheet = false,
navigateToUploadProgress = {},
sendTransfer = {},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,8 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
Expand All @@ -43,25 +40,33 @@ import com.infomaniak.swisstransfer.ui.theme.Margin
import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme
import com.infomaniak.swisstransfer.ui.utils.GetSetCallbacks
import com.infomaniak.swisstransfer.ui.utils.PreviewAllWindows
import com.infomaniak.swisstransfer.workers.UploadWorker
import com.infomaniak.swisstransfer.workers.UploadWorker.UploadProgressUiState

@Composable
fun UploadProgressScreen(
uploadProgressViewModel: UploadProgressViewModel = hiltViewModel<UploadProgressViewModel>(),
totalSizeInBytes: Long,
navigateToUploadSuccess: (String) -> Unit,
closeActivity: () -> Unit,
) {
val progress by uploadProgressViewModel.progress.collectAsStateWithLifecycle()
val uiState by uploadProgressViewModel.transferProgressUiState.collectAsStateWithLifecycle()

val adScreenType = rememberSaveable { UploadProgressAdType.entries.random() }

var showBottomSheet by rememberSaveable { mutableStateOf(false) }

BackHandler(!showBottomSheet) {
showBottomSheet = true
}

LaunchedEffect(Unit) {
uploadProgressViewModel.trackUploadProgress()
}

HandleProgressState({ uiState }, navigateToUploadSuccess)

UploadProgressScreen(
progress = { progress },
progressState = { uiState },
totalSizeInBytes = totalSizeInBytes,
showBottomSheet = GetSetCallbacks(get = { showBottomSheet }, set = { showBottomSheet = it }),
adScreenType = adScreenType,
onCancel = {
Expand All @@ -71,9 +76,29 @@ fun UploadProgressScreen(
)
}

@Composable
private fun HandleProgressState(
uiState: () -> UploadProgressUiState,
navigateToUploadSuccess: (String) -> Unit
) {
val currentUiState = uiState()
LaunchedEffect(uiState()) {
when (currentUiState) {
is UploadProgressUiState.Success -> {
navigateToUploadSuccess(currentUiState.transferLink)
}
is UploadProgressUiState.Cancelled -> {
// TODO: navigate to failure screen
}
else -> Unit
}
}
}

@Composable
private fun UploadProgressScreen(
progress: () -> UploadWorker.UploadTransferProgress,
progressState: () -> UploadProgressUiState,
totalSizeInBytes: Long,
adScreenType: UploadProgressAdType,
onCancel: () -> Unit,
showBottomSheet: GetSetCallbacks<Boolean>,
Expand All @@ -92,7 +117,7 @@ private fun UploadProgressScreen(

Spacer(modifier = Modifier.height(Margin.Medium))
Text(stringResource(R.string.uploadSuccessTransferInProgress))
Progress(progress)
Progress(progressState, totalSizeInBytes)
Spacer(modifier = Modifier.height(Margin.Huge))
}

Expand Down Expand Up @@ -130,7 +155,8 @@ private fun CancelUploadBottomSheet(onCancel: () -> Unit, closeButtonSheet: () -
private fun UploadProgressScreenPreview() {
SwissTransferTheme {
UploadProgressScreen(
progress = { UploadWorker.UploadTransferProgress(44_321_654L, 76_321_894L) },
progressState = { UploadProgressUiState.Progress(44_321_654L) },
totalSizeInBytes = 76_321_894L,
adScreenType = UploadProgressAdType.INDEPENDENCE,
onCancel = {},
showBottomSheet = GetSetCallbacks(get = { false }, set = { }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ package com.infomaniak.swisstransfer.ui.screen.newtransfer.upload
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.infomaniak.multiplatform_swisstransfer.managers.UploadManager
import com.infomaniak.sentry.SentryLog
import com.infomaniak.swisstransfer.di.IoDispatcher
import com.infomaniak.swisstransfer.workers.UploadWorker
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand All @@ -36,13 +36,31 @@ class UploadProgressViewModel @Inject constructor(
private val uploadManager: UploadManager,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
val progress = uploadWorkerScheduler.trackUploadProgressFlow()
.flowOn(ioDispatcher)
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = UploadWorker.UploadTransferProgress(0, 0)
)

private val _transferUuidFlow = MutableSharedFlow<String?>()

@OptIn(ExperimentalCoroutinesApi::class)
val transferProgressUiState = _transferUuidFlow.flatMapLatest { transferUuid ->
when (transferUuid) {
null -> flow { emit(UploadWorker.UploadProgressUiState.Cancelled()) }
else -> uploadWorkerScheduler.trackUploadProgressFlow(transferUuid).flowOn(ioDispatcher)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = UploadWorker.UploadProgressUiState.Default
)

fun trackUploadProgress() {
viewModelScope.launch(ioDispatcher) {
runCatching {
_transferUuidFlow.emit(uploadManager.getLastUpload()?.uuid)
}.onFailure {
SentryLog.e(TAG, "Failed to retrieve the last upload", it)
_transferUuidFlow.emit(null)
}
}
}

fun cancelUpload() {
uploadWorkerScheduler.cancelWork()
Expand All @@ -53,4 +71,8 @@ class UploadProgressViewModel @Inject constructor(
}
}
}

companion object {
private val TAG = UploadProgressViewModel::class.java.simpleName
}
}
Loading

0 comments on commit f0b6969

Please sign in to comment.