From 9a1d8b38d64116f3640eaeea408ea8eefeb54940 Mon Sep 17 00:00:00 2001 From: Gibran Chevalley Date: Wed, 18 Dec 2024 10:11:41 +0100 Subject: [PATCH] feat: Factorize send transfer logic to a reusable class --- .../newtransfer/ImportFilesViewModel.kt | 92 ++---------- .../screen/newtransfer/TransferSendManager.kt | 142 ++++++++++++++++++ .../importfiles/ImportFilesScreen.kt | 12 +- 3 files changed, 158 insertions(+), 88 deletions(-) create mode 100644 app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/TransferSendManager.kt diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt index e34cf2667..ca2a72d18 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt @@ -18,7 +18,6 @@ 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 @@ -26,9 +25,6 @@ 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 @@ -36,7 +32,6 @@ 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 @@ -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.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 @@ -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() diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/TransferSendManager.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/TransferSendManager.kt new file mode 100644 index 000000000..3f03691b7 --- /dev/null +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/TransferSendManager.kt @@ -0,0 +1,142 @@ +/* + * 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 . + */ +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, +) { + + private val _sendActionResult = MutableStateFlow(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 + } +} diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt index f8b338d51..2e40b756b 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/importfiles/ImportFilesScreen.kt @@ -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, @@ -202,7 +202,7 @@ private fun ImportFilesScreen( closeActivity: () -> Unit, shouldStartByPromptingUserForFiles: Boolean, integrityCheckResult: () -> AppIntegrityResult, - checkAppIntegrity: () -> Unit, + sendTransfer: () -> Unit, isTransferStarted: () -> Boolean, snackbarHostState: SnackbarHostState? = null, ) { @@ -226,7 +226,7 @@ private fun ImportFilesScreen( shouldShowEmailAddressesFields = { shouldShowEmailAddressesFields }, transferAuthorEmail = transferAuthorEmail, integrityCheckResult = integrityCheckResult, - checkAppIntegrityThenSendTransfer = checkAppIntegrity, + sendTransfer = sendTransfer, isTransferStarted = isTransferStarted, ) }, @@ -406,7 +406,7 @@ private fun SendButton( shouldShowEmailAddressesFields: () -> Boolean, transferAuthorEmail: GetSetCallbacks, integrityCheckResult: () -> AppIntegrityResult, - checkAppIntegrityThenSendTransfer: () -> Unit, + sendTransfer: () -> Unit, isTransferStarted: () -> Boolean, ) { val remainingFilesCount = filesToImportCount() @@ -431,7 +431,7 @@ private fun SendButton( showIndeterminateProgress = { integrityCheckResult() == AppIntegrityResult.Ongoing || isTransferStarted() }, enabled = { importedFiles().isNotEmpty() && !isImporting && isSenderEmailCorrect && !isTransferStarted() }, progress = progress, - onClick = { checkAppIntegrityThenSendTransfer() }, + onClick = { sendTransfer() }, ) } @@ -498,7 +498,7 @@ private fun Preview(@PreviewParameter(FileUiListPreviewParameter::class) files: closeActivity = {}, shouldStartByPromptingUserForFiles = false, integrityCheckResult = { AppIntegrityResult.Idle }, - checkAppIntegrity = {}, + sendTransfer = {}, isTransferStarted = { false }, ) }