Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into mail-validation-ui
Browse files Browse the repository at this point in the history
# Conflicts:
#	app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt
  • Loading branch information
LunarX committed Dec 18, 2024
2 parents e776e53 + 63f87c3 commit 3084096
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 147 deletions.
15 changes: 9 additions & 6 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import org.jetbrains.kotlin.konan.properties.loadProperties
import java.util.Properties

plugins {
alias(libs.plugins.android.application)
Expand All @@ -15,9 +15,12 @@ val sharedCompileSdk: Int by rootProject.extra
val sharedMinSdk: Int by rootProject.extra
val sharedJavaVersion: JavaVersion by rootProject.extra

val envProperties = loadProperties("env.properties")
val sentryAuthToken = envProperties.getProperty("sentryAuthToken").takeIf { it.isNotEmpty() }
?: error("The `sentryAuthToken` property in `env.properties` must be defined and not empty (see `env.example.properties`).")
val envProperties = rootProject.file("env.properties").takeIf { it.exists() }?.let { file ->
Properties().also { it.load(file.reader()) }
}

val sentryAuthToken = envProperties?.getProperty("sentryAuthToken").takeUnless { it.isNullOrBlank() }
?: error("The `sentryAuthToken` property in `env.properties` must be specified (see `env.example.properties`).")

android {
namespace = "com.infomaniak.swisstransfer"
Expand All @@ -27,8 +30,8 @@ android {
applicationId = "com.infomaniak.swisstransfer"
minSdk = sharedMinSdk
targetSdk = sharedCompileSdk
versionCode = 1 // 0_00_000_01 TODO: Update when released in prod
versionName = "0.0.1-Alpha1"
versionCode = 1_00_000_00
versionName = "1.0.0-Alpha2"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/
package com.infomaniak.swisstransfer.ui.components

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
Expand All @@ -27,17 +28,25 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.infomaniak.swisstransfer.ui.theme.CustomShapes
import com.infomaniak.swisstransfer.ui.theme.Margin
import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme
import com.infomaniak.swisstransfer.ui.utils.PreviewLightAndDark

@Composable
fun SwissTransferCard(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
fun SwissTransferCard(modifier: Modifier = Modifier, onClick: (() -> Unit)? = null, content: @Composable ColumnScope.() -> Unit) {
Card(
modifier = modifier,
shape = CustomShapes.MEDIUM,
modifier = modifier
.clip(shape = CustomShapes.MEDIUM)
.then(
other = if (onClick == null) {
Modifier
} else {
Modifier.clickable(onClick = onClick)
}
),
colors = CardDefaults.cardColors(contentColor = SwissTransferTheme.colors.secondaryTextColor),
content = content,
)
Expand All @@ -51,7 +60,7 @@ private fun SwissTransferCardPreview() {
SwissTransferCard(
modifier = Modifier
.size(300.dp, 200.dp)
.padding(Margin.Medium)
.padding(Margin.Medium),
) {
Column(Modifier.padding(Margin.Large)) {
Text("Hello World!")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,17 @@
*/
package com.infomaniak.swisstransfer.ui.navigation

import android.os.Bundle
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.runtime.Composable
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import androidx.navigation.toRoute
import com.infomaniak.sentry.SentryLog
import com.infomaniak.swisstransfer.BuildConfig
import com.infomaniak.swisstransfer.ui.screen.newtransfer.importfiles.components.TransferTypeUi
import kotlinx.serialization.Serializable
import kotlin.reflect.KClass
import kotlin.reflect.full.primaryConstructor

/**
* Sealed class representing the navigation arguments for the main navigation flow.
Expand All @@ -37,9 +36,11 @@ import kotlin.reflect.full.primaryConstructor
sealed class MainNavigation : NavigationDestination() {
var enableTransition = true

// If it has to be renamed, don't forget to rename `*DestinationName` in the companion object too.
@Serializable
data object SentDestination : MainNavigation()

// If it has to be renamed, don't forget to rename `*DestinationName` in the companion object too.
@Serializable
data class ReceivedDestination(val transferUuid: String? = null) : MainNavigation() {

Expand All @@ -57,15 +58,35 @@ sealed class MainNavigation : NavigationDestination() {
}
}
}

@Serializable
data class TransferDetailsDestination(val transferUuid: String) : MainNavigation()

// If it has to be renamed, don't forget to rename `*DestinationName` in the companion object too.
@Serializable
data object SettingsDestination : MainNavigation()

companion object {
private val TAG = MainNavigation::class.java.simpleName
val startDestination = SentDestination

// If these classes have to be renamed, they need to be renamed here too.
val sentDestinationName = "SentDestination"
val receivedDestinationName = "ReceivedDestination"
val settingsDestinationName = "SettingsDestination"
val destinationsNames = listOf(sentDestinationName, receivedDestinationName, settingsDestinationName)

fun NavBackStackEntry.toMainDestination(): MainNavigation? {
return runCatching {
val destinationRoute = destination.route ?: error("Destination route cannot be empty")
when (destinationsNames.firstOrNull { destinationRoute.contains(it) }) {
sentDestinationName -> this.toRoute<SentDestination>()
receivedDestinationName -> this.toRoute<ReceivedDestination>()
settingsDestinationName -> this.toRoute<SettingsDestination>()
else -> error("Destination $destinationRoute is not handled")
}
}.getOrElse { exception ->
SentryLog.e(TAG, "toMainDestination: Failure", exception)
null
}
}
}
}

Expand Down Expand Up @@ -98,34 +119,4 @@ sealed class NewTransferNavigation : NavigationDestination() {
* Sealed class representing navigation arguments with a title resource.
*/
@Serializable
sealed class NavigationDestination {

companion object {

inline fun <reified T : NavigationDestination> NavBackStackEntry.toDestination(): T? {
return toDestination(T::class, backStackEntry = this)
}

fun <T : NavigationDestination> toDestination(kClass: KClass<T>, backStackEntry: NavBackStackEntry?): T? {

fun kClassFromRoute(route: String) = kClass.sealedSubclasses.firstOrNull {
route.contains(it.qualifiedName.toString())
}

if (backStackEntry == null) return null

val route = backStackEntry.destination.route ?: ""
val args = backStackEntry.arguments
val subclass = kClassFromRoute(route) ?: return null

return createInstance(subclass, args)
}

private fun <T : NavigationDestination> createInstance(kClass: KClass<T>, bundle: Bundle?): T? {
return kClass.primaryConstructor?.let {
val args = it.parameters.associateWith { parameter -> bundle?.get(parameter.name) }
it.callBy(args)
} ?: kClass.objectInstance
}
}
}
sealed class NavigationDestination
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.infomaniak.swisstransfer.ui.components.BrandTopAppBar
import com.infomaniak.swisstransfer.ui.navigation.MainNavigation
import com.infomaniak.swisstransfer.ui.navigation.NavigationDestination.Companion.toDestination
import com.infomaniak.swisstransfer.ui.navigation.MainNavigation.Companion.toMainDestination
import com.infomaniak.swisstransfer.ui.screen.main.components.MainScaffold
import com.infomaniak.swisstransfer.ui.theme.SwissTransferTheme
import com.infomaniak.swisstransfer.ui.utils.PreviewAllWindows
Expand All @@ -37,7 +37,7 @@ fun MainScreen() {
val navBackStackEntry by navController.currentBackStackEntryAsState()

val currentDestination by remember(navBackStackEntry) {
derivedStateOf { navBackStackEntry?.toDestination<MainNavigation>() ?: MainNavigation.startDestination }
derivedStateOf { navBackStackEntry?.toMainDestination() ?: MainNavigation.startDestination }
}

MainScaffold(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
*/
package com.infomaniak.swisstransfer.ui.screen.main.components

import android.content.Intent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.HorizontalDivider
Expand All @@ -26,10 +25,8 @@ import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import com.infomaniak.core2.extensions.parcelableExtra
import com.infomaniak.swisstransfer.ui.components.BrandTopAppBar
import com.infomaniak.swisstransfer.ui.navigation.MainNavigation
import com.infomaniak.swisstransfer.ui.navigation.NavigationItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +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.multiplatform_swisstransfer.network.exceptions.ContainerErrorsException
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 @@ -52,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 @@ -139,86 +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)
val result = when (exception) {
is ContainerErrorsException.EmailValidationRequired -> SendActionResult.RequireEmailValidation
else -> SendActionResult.Failure
}
_sendActionResult.update { result }
}
transferSendManager.sendTransfer(generateNewUploadSession())
}
}

fun setDefaultSendActionResult() {
_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.setDefaultSendActionResult()
}

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

private suspend fun removeOldData() {
importationFilesManager.removeLocalCopyFolder()
Expand Down
Loading

0 comments on commit 3084096

Please sign in to comment.