Skip to content

Commit

Permalink
feat: Factorize send transfer logic to a reusable class (#267)
Browse files Browse the repository at this point in the history
  • Loading branch information
FabianDevel authored Dec 18, 2024
2 parents e8bb34f + c83d626 commit 63f87c3
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,20 @@
package com.infomaniak.swisstransfer.ui.screen.newtransfer

import android.net.Uri
import android.util.Log
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.infomaniak.core2.appintegrity.AppIntegrityManager
import com.infomaniak.core2.appintegrity.AppIntegrityManager.Companion.APP_INTEGRITY_MANAGER_TAG
import com.infomaniak.multiplatform_swisstransfer.SharedApiUrlCreator
import com.infomaniak.multiplatform_swisstransfer.common.interfaces.upload.RemoteUploadFile
import com.infomaniak.multiplatform_swisstransfer.common.interfaces.upload.UploadFileSession
import com.infomaniak.multiplatform_swisstransfer.common.utils.mapToList
import com.infomaniak.multiplatform_swisstransfer.data.NewUploadSession
import com.infomaniak.multiplatform_swisstransfer.managers.AppSettingsManager
import com.infomaniak.multiplatform_swisstransfer.managers.UploadManager
import com.infomaniak.sentry.SentryLog
import com.infomaniak.swisstransfer.BuildConfig
import com.infomaniak.swisstransfer.di.IoDispatcher
import com.infomaniak.swisstransfer.ui.screen.main.settings.DownloadLimitOption
import com.infomaniak.swisstransfer.ui.screen.main.settings.DownloadLimitOption.Companion.toTransferOption
Expand All @@ -51,31 +46,27 @@ import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.TransferOp
import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.components.TransferTypeUi
import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.components.TransferTypeUi.Companion.toTransferTypeUi
import com.infomaniak.swisstransfer.ui.utils.GetSetCallbacks
import com.infomaniak.swisstransfer.workers.UploadWorker
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class ImportFilesViewModel @Inject constructor(
private val appSettingsManager: AppSettingsManager,
private val appIntegrityManager: AppIntegrityManager,
private val savedStateHandle: SavedStateHandle,
private val importationFilesManager: ImportationFilesManager,
private val sharedApiUrlCreator: SharedApiUrlCreator,
private val uploadManager: UploadManager,
private val uploadWorkerScheduler: UploadWorker.Scheduler,
private val transferSendManager: TransferSendManager,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {

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

private val _integrityCheckResult = MutableStateFlow(AppIntegrityResult.Idle)
val integrityCheckResult = _integrityCheckResult.asStateFlow()
val sendActionResult by transferSendManager::sendActionResult
val integrityCheckResult by transferSendManager::integrityCheckResult

@OptIn(FlowPreview::class)
val importedFilesDebounced = importationFilesManager.importedFiles
Expand Down Expand Up @@ -138,82 +129,19 @@ class ImportFilesViewModel @Inject constructor(
}
}

private fun sendTransfer(attestationToken: String) {
_sendActionResult.update { SendActionResult.Pending }
fun sendTransfer() {
viewModelScope.launch(ioDispatcher) {
runCatching {
val uuid = uploadManager.createAndGetUpload(generateNewUploadSession()).uuid
uploadManager.initUploadSession(
attestationHeaderName = AppIntegrityManager.ATTESTATION_TOKEN_HEADER,
attestationToken = attestationToken,
)!! // TODO: Handle ContainerErrorsException here
uploadWorkerScheduler.scheduleWork(uuid)
_sendActionResult.update {
val totalSize = importationFilesManager.importedFiles.value.sumOf { it.fileSize }
SendActionResult.Success(totalSize)
}
}.onFailure { exception ->
SentryLog.e(TAG, "Failed to start the upload", exception)
_sendActionResult.update { SendActionResult.Failure }
}
transferSendManager.sendTransfer(generateNewUploadSession())
}
}

fun resetSendActionResult() {
_sendActionResult.value = SendActionResult.NotStarted
}

//region App Integrity
fun checkAppIntegrity() {
_integrityCheckResult.value = AppIntegrityResult.Ongoing
viewModelScope.launch(ioDispatcher) {
runCatching {
appIntegrityManager.getChallenge(
onSuccess = { requestAppIntegrityToken(appIntegrityManager) },
onFailure = ::setFailedIntegrityResult,
)
}.onFailure { exception ->
SentryLog.e(TAG, "Failed to start the upload", exception)
_sendActionResult.update { SendActionResult.Failure }
}
}
}

private fun requestAppIntegrityToken(appIntegrityManager: AppIntegrityManager) {
appIntegrityManager.requestClassicIntegrityVerdictToken(
onSuccess = { token ->
SentryLog.i(APP_INTEGRITY_MANAGER_TAG, "request for app integrity token successful $token")
getApiIntegrityVerdict(appIntegrityManager, token)
},
onFailure = ::setFailedIntegrityResult,
)
}

private fun getApiIntegrityVerdict(appIntegrityManager: AppIntegrityManager, appIntegrityToken: String) {
viewModelScope.launch(ioDispatcher) {
appIntegrityManager.getApiIntegrityVerdict(
integrityToken = appIntegrityToken,
packageName = BuildConfig.APPLICATION_ID,
targetUrl = sharedApiUrlCreator.createUploadContainerUrl,
onSuccess = { attestationToken ->
SentryLog.i(APP_INTEGRITY_MANAGER_TAG, "Api verdict check")
Log.i(APP_INTEGRITY_MANAGER_TAG, "getApiIntegrityVerdict: $attestationToken")
_integrityCheckResult.value = AppIntegrityResult.Success
sendTransfer(attestationToken)
},
onFailure = ::setFailedIntegrityResult,
)
}
}

private fun setFailedIntegrityResult() {
_integrityCheckResult.value = AppIntegrityResult.Fail
transferSendManager.resetSendActionResult()
}

fun resetIntegrityCheckResult() {
_integrityCheckResult.value = AppIntegrityResult.Idle
transferSendManager.resetIntegrityCheckResult()
}
//endregion

private suspend fun removeOldData() {
importationFilesManager.removeLocalCopyFolder()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Infomaniak SwissTransfer - Android
* Copyright (C) 2024 Infomaniak Network SA
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.swisstransfer.ui.screen.newtransfer

import android.util.Log
import com.infomaniak.core2.appintegrity.AppIntegrityManager
import com.infomaniak.core2.appintegrity.AppIntegrityManager.Companion.APP_INTEGRITY_MANAGER_TAG
import com.infomaniak.multiplatform_swisstransfer.SharedApiUrlCreator
import com.infomaniak.multiplatform_swisstransfer.data.NewUploadSession
import com.infomaniak.multiplatform_swisstransfer.managers.UploadManager
import com.infomaniak.sentry.SentryLog
import com.infomaniak.swisstransfer.BuildConfig
import com.infomaniak.swisstransfer.ui.screen.newtransfer.ImportFilesViewModel.AppIntegrityResult
import com.infomaniak.swisstransfer.ui.screen.newtransfer.ImportFilesViewModel.SendActionResult
import com.infomaniak.swisstransfer.workers.UploadWorker
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

@ViewModelScoped
class TransferSendManager @Inject constructor(
private val appIntegrityManager: AppIntegrityManager,
private val importationFilesManager: ImportationFilesManager,
private val sharedApiUrlCreator: SharedApiUrlCreator,
private val uploadManager: UploadManager,
private val uploadWorkerScheduler: UploadWorker.Scheduler,
) {

// TODO: Merge these two ui states in a single one for the whole flow of logic
private val _sendActionResult = MutableStateFlow<SendActionResult?>(SendActionResult.NotStarted)
val sendActionResult = _sendActionResult.asStateFlow()

private val _integrityCheckResult = MutableStateFlow(AppIntegrityResult.Idle)
val integrityCheckResult = _integrityCheckResult.asStateFlow()

//region App Integrity
suspend fun sendTransfer(newUploadSession: NewUploadSession) {
_integrityCheckResult.value = AppIntegrityResult.Ongoing
coroutineScope {
runCatching {
appIntegrityManager.getChallenge(
onSuccess = { requestAppIntegrityToken(appIntegrityManager, newUploadSession) },
onFailure = ::setFailedIntegrityResult,
)
}.onFailure { exception ->
SentryLog.e(TAG, "Failed to start the upload", exception)
_sendActionResult.update { SendActionResult.Failure }
}
}
}

private fun CoroutineScope.requestAppIntegrityToken(
appIntegrityManager: AppIntegrityManager,
newUploadSession: NewUploadSession,
) {
appIntegrityManager.requestClassicIntegrityVerdictToken(
onSuccess = { token ->
SentryLog.i(APP_INTEGRITY_MANAGER_TAG, "request for app integrity token successful $token")
getApiIntegrityVerdict(appIntegrityManager, token, newUploadSession)
},
onFailure = ::setFailedIntegrityResult,
)
}

private fun CoroutineScope.getApiIntegrityVerdict(
appIntegrityManager: AppIntegrityManager,
appIntegrityToken: String,
newUploadSession: NewUploadSession,
) {
launch {
appIntegrityManager.getApiIntegrityVerdict(
integrityToken = appIntegrityToken,
packageName = BuildConfig.APPLICATION_ID,
targetUrl = sharedApiUrlCreator.createUploadContainerUrl,
onSuccess = { attestationToken ->
SentryLog.i(APP_INTEGRITY_MANAGER_TAG, "Api verdict check")
Log.i(APP_INTEGRITY_MANAGER_TAG, "getApiIntegrityVerdict: $attestationToken")
_integrityCheckResult.value = AppIntegrityResult.Success
sendTransfer(attestationToken, newUploadSession)
},
onFailure = ::setFailedIntegrityResult,
)
}
}

private fun setFailedIntegrityResult() {
_integrityCheckResult.value = AppIntegrityResult.Fail
}

fun resetIntegrityCheckResult() {
_integrityCheckResult.value = AppIntegrityResult.Idle
}
//endregion

fun resetSendActionResult() {
_sendActionResult.value = SendActionResult.NotStarted
}

private fun CoroutineScope.sendTransfer(attestationToken: String, newUploadSession: NewUploadSession) {
_sendActionResult.update { SendActionResult.Pending }
launch {
runCatching {
val uuid = uploadManager.createAndGetUpload(newUploadSession).uuid
uploadManager.initUploadSession(
attestationHeaderName = AppIntegrityManager.ATTESTATION_TOKEN_HEADER,
attestationToken = attestationToken,
)!! // TODO: Handle ContainerErrorsException here
uploadWorkerScheduler.scheduleWork(uuid)
_sendActionResult.update {
val totalSize = importationFilesManager.importedFiles.value.sumOf { it.fileSize }
SendActionResult.Success(totalSize)
}
}.onFailure { exception ->
SentryLog.e(TAG, "Failed to start the upload", exception)
_sendActionResult.update { SendActionResult.Failure }
}
}
}

companion object {
private val TAG = TransferSendManager::class.java.simpleName
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ fun ImportFilesScreen(
addFiles = importFilesViewModel::importFiles,
closeActivity = closeActivity,
integrityCheckResult = { integrityCheckResult },
checkAppIntegrity = importFilesViewModel::checkAppIntegrity,
sendTransfer = importFilesViewModel::sendTransfer,
shouldStartByPromptingUserForFiles = true,
isTransferStarted = { sendActionResult != SendActionResult.NotStarted },
snackbarHostState = snackbarHostState,
Expand Down Expand Up @@ -202,7 +202,7 @@ private fun ImportFilesScreen(
closeActivity: () -> Unit,
shouldStartByPromptingUserForFiles: Boolean,
integrityCheckResult: () -> AppIntegrityResult,
checkAppIntegrity: () -> Unit,
sendTransfer: () -> Unit,
isTransferStarted: () -> Boolean,
snackbarHostState: SnackbarHostState? = null,
) {
Expand All @@ -226,7 +226,7 @@ private fun ImportFilesScreen(
shouldShowEmailAddressesFields = { shouldShowEmailAddressesFields },
transferAuthorEmail = transferAuthorEmail,
integrityCheckResult = integrityCheckResult,
checkAppIntegrityThenSendTransfer = checkAppIntegrity,
sendTransfer = sendTransfer,
isTransferStarted = isTransferStarted,
)
},
Expand Down Expand Up @@ -406,7 +406,7 @@ private fun SendButton(
shouldShowEmailAddressesFields: () -> Boolean,
transferAuthorEmail: GetSetCallbacks<String>,
integrityCheckResult: () -> AppIntegrityResult,
checkAppIntegrityThenSendTransfer: () -> Unit,
sendTransfer: () -> Unit,
isTransferStarted: () -> Boolean,
) {
val remainingFilesCount = filesToImportCount()
Expand All @@ -431,7 +431,7 @@ private fun SendButton(
showIndeterminateProgress = { integrityCheckResult() == AppIntegrityResult.Ongoing || isTransferStarted() },
enabled = { importedFiles().isNotEmpty() && !isImporting && isSenderEmailCorrect && !isTransferStarted() },
progress = progress,
onClick = { checkAppIntegrityThenSendTransfer() },
onClick = { sendTransfer() },
)
}

Expand Down Expand Up @@ -498,7 +498,7 @@ private fun Preview(@PreviewParameter(FileUiListPreviewParameter::class) files:
closeActivity = {},
shouldStartByPromptingUserForFiles = false,
integrityCheckResult = { AppIntegrityResult.Idle },
checkAppIntegrity = {},
sendTransfer = {},
isTransferStarted = { false },
)
}
Expand Down

0 comments on commit 63f87c3

Please sign in to comment.