From 55212fc0dbb638233aa519b876578ecda51e58c1 Mon Sep 17 00:00:00 2001 From: Niko Date: Mon, 19 Aug 2024 09:30:49 +0200 Subject: [PATCH 1/4] feature(bank-sdk): Skonto Invoice Preview PP-643 --- .../bank/sdk/capture/CaptureFlowFragment.kt | 14 +- .../sdk/capture/CaptureFlowFragmentModule.kt | 8 + .../SkontoInvoiceHighlightsExtractor.kt | 78 +++++++ .../bank/sdk/capture/skonto/SkontoFragment.kt | 70 +++++- .../capture/skonto/SkontoFragmentContract.kt | 4 + .../capture/skonto/SkontoFragmentViewModel.kt | 9 +- .../skonto/invoice/SkontoInvoiceFragment.kt | 62 +++++ .../invoice/SkontoInvoiceFragmentState.kt | 9 + .../invoice/SkontoInvoiceFragmentViewModel.kt | 67 ++++++ .../skonto/invoice/SkontoInvoiceModule.kt | 53 +++++ .../SkontoInvoicePreviewScreenColors.kt | 46 ++++ .../skonto/invoice/SkontoInvoiceScreen.kt | 159 +++++++++++++ .../invoice/image/SkontoPageImageProcessor.kt | 69 ++++++ .../SkontoDocumentLayoutNetworkService.kt | 34 +++ .../SkontoDocumentPagesNetworkService.kt | 34 +++ .../network/SkontoFileNetworkService.kt | 33 +++ .../model/SkontoInvoiceHighlightBoxes.kt | 25 +++ .../bank/sdk/di/BankSdkIsolatedKoinContext.kt | 25 +++ .../bank/sdk/di/IsolatedKoinContext.kt | 15 -- .../src/main/res/navigation/gbs_nav_graph.xml | 63 ++++-- ...ResponseItemDetailsScreenPresenterTest.kt} | 2 +- .../GiniCaptureDefaultNetworkService.kt | 105 +++++++++ .../network/model/DocumentLayoutMapper.kt | 65 ++++++ .../network/model/DocumentPagesMapper.kt | 16 ++ capture-sdk/sdk/build.gradle.kts | 6 + .../android/capture/GiniCaptureFragment.kt | 4 +- .../capture/analysis/AnalysisInteractor.java | 30 ++- .../analysis/AnalysisScreenPresenter.java | 9 + .../AnalysisScreenPresenterExtension.kt | 10 + .../LastAnalyzedDocumentIdProvider.kt | 16 ++ .../di/CaptureSdkIsolatedKoinContext.kt | 14 ++ .../gini/android/capture/di/ProviderModule.kt | 8 + .../internal/network/model/DocumentLayout.kt | 57 +++++ .../internal/network/model/DocumentPage.kt | 13 ++ .../network/GiniCaptureNetworkService.java | 32 ++- .../ui/components/button/filled/GiniButton.kt | 4 +- .../ui/components/list/ZoomableLazyColumn.kt | 211 ++++++++++++++++++ .../analysis/AnalysisScreenPresenterTest.kt | 2 +- config/detekt/detekt.yml | 4 +- .../gini/android/core/api/DocumentManager.kt | 36 +++ .../android/core/api/DocumentRemoteSource.kt | 28 ++- .../android/core/api/DocumentRepository.kt | 20 ++ .../gini/android/core/api/DocumentService.kt | 17 +- .../core/api/mapper/DocumentLayoutMapper.kt | 48 ++++ .../core/api/mapper/DocumentPagesMapper.kt | 15 ++ .../android/core/api/models/DocumentLayout.kt | 57 +++++ .../android/core/api/models/DocumentPage.kt | 12 + .../api/response/DocumentLayoutResponse.kt | 67 ++++++ .../core/api/response/DocumentPageResponse.kt | 16 ++ gradle/libs.versions.toml | 1 + .../{PageTest.kt => PageResponseTest.kt} | 2 +- 51 files changed, 1736 insertions(+), 68 deletions(-) create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowFragmentModule.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/extractions/skonto/SkontoInvoiceHighlightsExtractor.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceFragment.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceFragmentState.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceFragmentViewModel.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceModule.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoicePreviewScreenColors.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceScreen.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/image/SkontoPageImageProcessor.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/network/SkontoDocumentLayoutNetworkService.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/network/SkontoDocumentPagesNetworkService.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/network/SkontoFileNetworkService.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/model/SkontoInvoiceHighlightBoxes.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/di/BankSdkIsolatedKoinContext.kt delete mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/di/IsolatedKoinContext.kt rename bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/digitalinvoice/details/{LineItemDetailsScreenPresenterTest.kt => LineResponseItemDetailsScreenPresenterTest.kt} (99%) create mode 100644 capture-sdk/default-network/src/main/java/net/gini/android/capture/network/model/DocumentLayoutMapper.kt create mode 100644 capture-sdk/default-network/src/main/java/net/gini/android/capture/network/model/DocumentPagesMapper.kt create mode 100644 capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/AnalysisScreenPresenterExtension.kt create mode 100644 capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/LastAnalyzedDocumentIdProvider.kt create mode 100644 capture-sdk/sdk/src/main/java/net/gini/android/capture/di/CaptureSdkIsolatedKoinContext.kt create mode 100644 capture-sdk/sdk/src/main/java/net/gini/android/capture/di/ProviderModule.kt create mode 100644 capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/model/DocumentLayout.kt create mode 100644 capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/model/DocumentPage.kt create mode 100644 capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/list/ZoomableLazyColumn.kt create mode 100644 core-api-library/library/src/main/java/net/gini/android/core/api/mapper/DocumentLayoutMapper.kt create mode 100644 core-api-library/library/src/main/java/net/gini/android/core/api/mapper/DocumentPagesMapper.kt create mode 100644 core-api-library/library/src/main/java/net/gini/android/core/api/models/DocumentLayout.kt create mode 100644 core-api-library/library/src/main/java/net/gini/android/core/api/models/DocumentPage.kt create mode 100644 core-api-library/library/src/main/java/net/gini/android/core/api/response/DocumentLayoutResponse.kt create mode 100644 core-api-library/library/src/main/java/net/gini/android/core/api/response/DocumentPageResponse.kt rename health-api-library/library/src/test/java/net/gini/android/health/api/models/{PageTest.kt => PageResponseTest.kt} (99%) diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowFragment.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowFragment.kt index 4da550f029..386c97e68f 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowFragment.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowFragment.kt @@ -16,9 +16,11 @@ import net.gini.android.bank.sdk.capture.digitalinvoice.DigitalInvoiceException import net.gini.android.bank.sdk.capture.digitalinvoice.DigitalInvoiceFragment import net.gini.android.bank.sdk.capture.digitalinvoice.DigitalInvoiceFragmentListener import net.gini.android.bank.sdk.capture.digitalinvoice.LineItemsValidator +import net.gini.android.bank.sdk.capture.extractions.skonto.SkontoInvoiceHighlightsExtractor import net.gini.android.bank.sdk.capture.skonto.SkontoDataExtractor import net.gini.android.bank.sdk.capture.skonto.SkontoFragment import net.gini.android.bank.sdk.capture.skonto.SkontoFragmentListener +import net.gini.android.bank.sdk.di.getGiniBankKoin import net.gini.android.bank.sdk.util.disallowScreenshots import net.gini.android.capture.CaptureSDKResult @@ -47,6 +49,9 @@ class CaptureFlowFragment(private val openWithDocument: Document? = null) : private lateinit var navController: NavController private lateinit var captureFlowFragmentListener: CaptureFlowFragmentListener + private val skontoInvoiceHighlightsExtractor: SkontoInvoiceHighlightsExtractor + by getGiniBankKoin().inject() + // Remember the original primary navigation fragment so that we can restore it when this fragment is detached private var originalPrimaryNavigationFragment: Fragment? = null @@ -191,8 +196,15 @@ class CaptureFlowFragment(private val openWithDocument: Document? = null) : result.compoundExtractions ) + val highlightBoxes = skontoInvoiceHighlightsExtractor.extract( + result.compoundExtractions + ) + navController.navigate( - GiniCaptureFragmentDirections.toSkontoFragment(data = skontoData) + GiniCaptureFragmentDirections.toSkontoFragment( + data = skontoData, + invoiceHighlights = highlightBoxes.toTypedArray(), + ) ) } catch (e: Exception) { finishWithResult(result) diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowFragmentModule.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowFragmentModule.kt new file mode 100644 index 0000000000..cf0151c0d6 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowFragmentModule.kt @@ -0,0 +1,8 @@ +package net.gini.android.bank.sdk.capture + +import net.gini.android.bank.sdk.capture.extractions.skonto.SkontoInvoiceHighlightsExtractor +import org.koin.dsl.module + +val captureFlowFragmentModule = module { + single { SkontoInvoiceHighlightsExtractor() } +} diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/extractions/skonto/SkontoInvoiceHighlightsExtractor.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/extractions/skonto/SkontoInvoiceHighlightsExtractor.kt new file mode 100644 index 0000000000..eaae15f140 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/extractions/skonto/SkontoInvoiceHighlightsExtractor.kt @@ -0,0 +1,78 @@ +package net.gini.android.bank.sdk.capture.extractions.skonto + +import net.gini.android.bank.sdk.capture.skonto.extractDataByKeys +import net.gini.android.bank.sdk.capture.skonto.model.SkontoInvoiceHighlightBoxes +import net.gini.android.capture.network.model.GiniCaptureCompoundExtraction +import net.gini.android.capture.network.model.GiniCaptureSpecificExtraction +import kotlin.jvm.Throws + +internal class SkontoInvoiceHighlightsExtractor { + + fun extract( + compoundExtractions: Map, + ): List { + + val skontoDiscountMaps = compoundExtractions["skontoDiscounts"]?.specificExtractionMaps + ?: throw NoSuchElementException("Field `compoundExtractions.skontoDiscounts` is missing") + + return skontoDiscountMaps.map { skontoDiscountData -> + val skontoPercentageDiscounted = extractPercentageDiscountedOrError(skontoDiscountData) + val skontoPaymentMethod = skontoDiscountData.extractDataByKeys("skontoPaymentMethod") + val skontoAmountToPay = extractAmountToPayOrError(skontoDiscountData) + val skontoRemainingDays = extractRemainingDaysOrError(skontoDiscountData) + + val skontoDueDate = extractDueDateOrError(skontoDiscountData) + + val skontoAmountDiscounted = skontoDiscountData.extractDataByKeys( + "skontoAmountDiscounted", + "skontoAmountDiscountedCalculated" + ) + + SkontoInvoiceHighlightBoxes( + skontoPercentageDiscounted = skontoPercentageDiscounted.box, + skontoPaymentMethod = skontoPaymentMethod?.box, + skontoRemainingDays = skontoRemainingDays.box, + skontoDueDate = skontoDueDate.box, + skontoAmountToPay = skontoAmountToPay.box, + skontoAmountDiscounted = skontoAmountDiscounted?.box, + ) + } + } + + @Throws(NoSuchElementException::class) + private fun extractPercentageDiscountedOrError( + skontoDiscountMap: Map + ): GiniCaptureSpecificExtraction = + skontoDiscountMap.extractDataByKeys( + "skontoPercentageDiscounted", + "skontoPercentageDiscountedCalculated", + ) ?: throw NoSuchElementException("Data for `PercentageDiscounted` is missing") + + @Throws(NoSuchElementException::class) + private fun extractAmountToPayOrError( + skontoDiscountMap: Map + ): GiniCaptureSpecificExtraction = + skontoDiscountMap.extractDataByKeys( + "skontoAmountToPay", + "skontoAmountToPayCalculated" + ) ?: throw NoSuchElementException("Skonto data for `AmountToPay` is missing") + + @Throws(NoSuchElementException::class) + private fun extractRemainingDaysOrError( + skontoDiscountMap: Map + ): GiniCaptureSpecificExtraction = + skontoDiscountMap.extractDataByKeys( + "skontoRemainingDays", + "skontoRemainingDaysCalculated" + ) ?: throw NoSuchElementException("Skonto data for `RemainingDays` is missing") + + @Throws(NoSuchElementException::class) + private fun extractDueDateOrError( + skontoDiscountMap: Map + ): GiniCaptureSpecificExtraction = + skontoDiscountMap.extractDataByKeys( + "skontoDueDate", + "skontoDueDateCalculated" + ) ?: throw NoSuchElementException("Skonto data for `DueDate` is missing") + +} diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt index e41ebd9a65..88f0aee6d0 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt @@ -2,6 +2,7 @@ package net.gini.android.bank.sdk.capture.skonto +import android.annotation.SuppressLint import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.icu.util.Calendar import android.os.Bundle @@ -57,6 +58,7 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource @@ -67,6 +69,8 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.findNavController import androidx.navigation.fragment.navArgs import net.gini.android.bank.sdk.GiniBank @@ -79,7 +83,7 @@ import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoSectionColo import net.gini.android.bank.sdk.capture.skonto.colors.section.WithoutSkontoSectionColors import net.gini.android.bank.sdk.capture.skonto.model.SkontoData import net.gini.android.bank.sdk.capture.util.currencyFormatterWithoutSymbol -import net.gini.android.bank.sdk.di.getGiniKoin +import net.gini.android.bank.sdk.di.getGiniBankKoin import net.gini.android.bank.sdk.util.disallowScreenshots import net.gini.android.capture.GiniCapture import net.gini.android.capture.internal.util.ActivityHelper.forcePortraitOrientationOnPhones @@ -105,7 +109,7 @@ class SkontoFragment : Fragment() { private val args: SkontoFragmentArgs by navArgs() - private val viewModel: SkontoFragmentViewModel by getGiniKoin().inject { + private val viewModel: SkontoFragmentViewModel by getGiniBankKoin().inject { parametersOf(args.data) } @@ -155,6 +159,14 @@ class SkontoFragment : Fragment() { navigateBack = { findNavController() .navigate(SkontoFragmentDirections.toCaptureFragment()) + }, + navigateToInvoiceScreen = { + findNavController() + .navigate( + SkontoFragmentDirections.toSkontoInvoiceFragment( + args.invoiceHighlights + ) + ) } ) } @@ -163,6 +175,23 @@ class SkontoFragment : Fragment() { } } +@Composable +@SuppressLint("ComposableNaming") +private fun SkontoFragmentViewModel.collectSideEffect( + action: (SkontoFragmentContract.SideEffect) -> Unit +) { + + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(sideEffectFlow, lifecycleOwner) { + lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + sideEffectFlow.collect { + action(it) + } + } + } +} + @Composable private fun ScreenContent( navigateBack: () -> Unit, @@ -171,11 +200,19 @@ private fun ScreenContent( screenColorScheme: SkontoScreenColors = SkontoScreenColors.colors(), isBottomNavigationBarEnabled: Boolean, customBottomNavBarAdapter: InjectedViewAdapterInstance?, + navigateToInvoiceScreen: () -> Unit, ) { BackHandler { navigateBack() } val state by viewModel.stateFlow.collectAsState() + + viewModel.collectSideEffect { + when (it) { + SkontoFragmentContract.SideEffect.OpenInvoiceScreen -> navigateToInvoiceScreen() + } + } + ScreenStateContent( modifier = modifier, state = state, @@ -187,9 +224,10 @@ private fun ScreenContent( isBottomNavigationBarEnabled = isBottomNavigationBarEnabled, onBackClicked = navigateBack, customBottomNavBarAdapter = customBottomNavBarAdapter, - onProceedClicked = { viewModel.onProceedClicked() }, + onProceedClicked = viewModel::onProceedClicked, onInfoBannerClicked = viewModel::onInfoBannerClicked, - onInfoDialogDismissed = viewModel::onInfoDialogDismissed + onInfoDialogDismissed = viewModel::onInfoDialogDismissed, + onInvoiceClicked = viewModel::onInvoiceClicked ) } @@ -206,6 +244,7 @@ private fun ScreenStateContent( customBottomNavBarAdapter: InjectedViewAdapterInstance?, onInfoBannerClicked: () -> Unit, onInfoDialogDismissed: () -> Unit, + onInvoiceClicked: () -> Unit, modifier: Modifier = Modifier, screenColorScheme: SkontoScreenColors = SkontoScreenColors.colors() ) { @@ -224,6 +263,7 @@ private fun ScreenStateContent( onProceedClicked = onProceedClicked, onInfoBannerClicked = onInfoBannerClicked, onInfoDialogDismissed = onInfoDialogDismissed, + onInvoiceClicked = onInvoiceClicked, ) } @@ -233,6 +273,7 @@ private fun ScreenStateContent( private fun ScreenReadyState( onBackClicked: () -> Unit, onProceedClicked: () -> Unit, + onInvoiceClicked: () -> Unit, state: SkontoFragmentContract.State.Ready, onDiscountSectionActiveChange: (Boolean) -> Unit, onDiscountAmountChange: (BigDecimal) -> Unit, @@ -276,12 +317,21 @@ private fun ScreenReadyState( horizontalAlignment = Alignment.CenterHorizontally, ) { Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), ) { + YourInvoiceScanSection( + modifier = Modifier + .padding(top = 8.dp) + .tabletMaxWidth(), + colorScheme = screenColorScheme.invoiceScanSectionColors, + onClick = onInvoiceClicked, + ) SkontoSection( modifier = Modifier - .padding(vertical = 16.dp) .tabletMaxWidth(), colors = screenColorScheme.skontoSectionColors, amount = state.skontoAmount, @@ -370,9 +420,12 @@ private fun NavigationActionBack( private fun YourInvoiceScanSection( modifier: Modifier = Modifier, colorScheme: SkontoInvoiceScanSectionColors, + onClick: () -> Unit, ) { Card( - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick), shape = RectangleShape, colors = CardDefaults.cardColors(containerColor = colorScheme.cardBackgroundColor) ) { @@ -932,6 +985,7 @@ private fun ScreenReadyStatePreview() { customBottomNavBarAdapter = null, onInfoDialogDismissed = {}, onInfoBannerClicked = {}, + onInvoiceClicked = {} ) } } diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentContract.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentContract.kt index a11620e306..cb2fdce689 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentContract.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentContract.kt @@ -22,6 +22,10 @@ internal object SkontoFragmentContract { ) : State() } + sealed interface SideEffect { + object OpenInvoiceScreen : SideEffect + } + sealed class SkontoEdgeCase { object SkontoLastDay : SkontoEdgeCase() object PayByCashOnly : SkontoEdgeCase() diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModel.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModel.kt index 606bb2b7b1..8734003a6c 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModel.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModel.kt @@ -2,6 +2,7 @@ package net.gini.android.bank.sdk.capture.skonto import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import net.gini.android.bank.sdk.capture.skonto.model.SkontoData @@ -18,6 +19,8 @@ internal class SkontoFragmentViewModel( val stateFlow: MutableStateFlow = MutableStateFlow(createInitalState(data)) + val sideEffectFlow: MutableSharedFlow = MutableSharedFlow() + private var listener: SkontoFragmentListener? = null fun setListener(listener: SkontoFragmentListener?) { @@ -175,6 +178,10 @@ internal class SkontoFragmentViewModel( ) } + fun onInvoiceClicked() = viewModelScope.launch { + sideEffectFlow.emit(SkontoFragmentContract.SideEffect.OpenInvoiceScreen) + } + private fun calculateDiscount(skontoAmount: BigDecimal, fullAmount: BigDecimal): BigDecimal { if (fullAmount == BigDecimal.ZERO) return BigDecimal("100") return BigDecimal.ONE @@ -205,4 +212,4 @@ internal class SkontoFragmentViewModel( else -> null } } -} \ No newline at end of file +} diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceFragment.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceFragment.kt new file mode 100644 index 0000000000..3cae21c1e3 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceFragment.kt @@ -0,0 +1,62 @@ +package net.gini.android.bank.sdk.capture.skonto.invoice + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import net.gini.android.bank.sdk.GiniBank +import net.gini.android.bank.sdk.capture.skonto.SkontoNavigationBarBottomAdapter +import net.gini.android.bank.sdk.di.getGiniBankKoin +import net.gini.android.bank.sdk.util.disallowScreenshots +import net.gini.android.capture.GiniCapture +import net.gini.android.capture.internal.util.ActivityHelper +import net.gini.android.capture.ui.theme.GiniTheme +import net.gini.android.capture.view.InjectedViewAdapterInstance +import org.koin.core.parameter.parametersOf + +class SkontoInvoiceFragment : Fragment() { + + private val args: SkontoInvoiceFragmentArgs by navArgs() + + private val viewModel: SkontoInvoiceFragmentViewModel by getGiniBankKoin().inject { + parametersOf(args.invoiceHighlights) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (GiniCapture.hasInstance() && !GiniCapture.getInstance().allowScreenshots) { + requireActivity().window.disallowScreenshots() + } + ActivityHelper.forcePortraitOrientationOnPhones(activity) + + if (resources.getBoolean(net.gini.android.capture.R.bool.gc_is_tablet)) { + requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + GiniTheme { + SkontoInvoiceScreen( + modifier = Modifier, + viewModel = viewModel, + navigateBack = { + findNavController().popBackStack() + } + ) + } + } + } + } +} diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceFragmentState.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceFragmentState.kt new file mode 100644 index 0000000000..7a7b9c0ca3 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceFragmentState.kt @@ -0,0 +1,9 @@ +package net.gini.android.bank.sdk.capture.skonto.invoice + +import android.graphics.Bitmap + +data class SkontoInvoiceFragmentState( + val isLoading: Boolean, + val images: List, + +) diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceFragmentViewModel.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceFragmentViewModel.kt new file mode 100644 index 0000000000..8007bdc128 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceFragmentViewModel.kt @@ -0,0 +1,67 @@ +package net.gini.android.bank.sdk.capture.skonto.invoice + +import android.graphics.BitmapFactory +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import net.gini.android.bank.sdk.capture.skonto.invoice.image.SkontoPageImageProcessor +import net.gini.android.bank.sdk.capture.skonto.invoice.network.SkontoDocumentLayoutNetworkService +import net.gini.android.bank.sdk.capture.skonto.invoice.network.SkontoDocumentPagesNetworkService +import net.gini.android.bank.sdk.capture.skonto.invoice.network.SkontoFileNetworkService +import net.gini.android.bank.sdk.capture.skonto.model.SkontoInvoiceHighlightBoxes + +internal class SkontoInvoiceFragmentViewModel( + private val documentId: String?, + private val skontoInvoiceHighlights: List, + private val skontoDocumentLayoutNetworkService: SkontoDocumentLayoutNetworkService, + private val skontoDocumentPagesNetworkService: SkontoDocumentPagesNetworkService, + private val skontoFileNetworkService: SkontoFileNetworkService, + private val skontoPageImageProcessor: SkontoPageImageProcessor, +) : ViewModel() { + + val stateFlow: MutableStateFlow = + MutableStateFlow(createInitalState()) + + private fun createInitalState() = + SkontoInvoiceFragmentState(isLoading = true, images = emptyList()) + + init { + init() + } + + private fun init() = viewModelScope.launch { + requireNotNull(documentId) + + val layout = skontoDocumentLayoutNetworkService.getLayout(documentId) + val pages = skontoDocumentPagesNetworkService.getDocumentPages(documentId) + + val bitmaps = pages.map { documentPage -> + val bitmapBytes = skontoFileNetworkService.getFile(documentPage.getSmallestImage()!!) + val bitmap = BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.size) + val pageHighlights = skontoInvoiceHighlights.find { + it.getExistBoxes().all { it.pageNumber == documentPage.pageNumber } + } + + val skontoPageLayout = layout.pages.find { documentPage.pageNumber == it.number } + + + pageHighlights?.let { + skontoPageImageProcessor.processImage( + image = bitmap, + skontoInvoiceHighlightBoxes = pageHighlights, + skontoPageLayout = skontoPageLayout + ?: error("Layout for page #$${documentPage.pageNumber} not found") + ) + } ?: bitmap + } + + val currentState = stateFlow.value + stateFlow.emit( + currentState.copy( + isLoading = false, + images = bitmaps + ) + ) + } +} diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceModule.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceModule.kt new file mode 100644 index 0000000000..6b3247f111 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceModule.kt @@ -0,0 +1,53 @@ +package net.gini.android.bank.sdk.capture.skonto.invoice + +import net.gini.android.bank.sdk.capture.skonto.invoice.image.SkontoPageImageProcessor +import net.gini.android.capture.GiniCapture +import net.gini.android.bank.sdk.capture.skonto.invoice.network.SkontoDocumentLayoutNetworkService +import net.gini.android.bank.sdk.capture.skonto.invoice.network.SkontoDocumentPagesNetworkService +import net.gini.android.bank.sdk.capture.skonto.invoice.network.SkontoFileNetworkService +import net.gini.android.bank.sdk.capture.skonto.model.SkontoInvoiceHighlightBoxes +import net.gini.android.capture.analysis.LastAnalyzedDocumentIdProvider +import net.gini.android.capture.di.getGiniCaptureKoin +import net.gini.android.capture.network.GiniCaptureDefaultNetworkService +import net.gini.android.capture.network.GiniCaptureNetworkService +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val skontoInvoiceScreenModule = module { + + factory { + GiniCapture.getInstance() + .internal().giniCaptureNetworkService + ?: error("GiniCaptureNetworkService should be initialized") + } + + factory { + SkontoFileNetworkService(get()) + } + + factory { + SkontoDocumentLayoutNetworkService(get()) + } + + factory { + SkontoDocumentPagesNetworkService(get()) + } + + factory { + SkontoPageImageProcessor() + } + + // Bridge between GiniCapture and GiniBank + factory { getGiniCaptureKoin().get() } + + viewModel { (highlights: Array) -> + SkontoInvoiceFragmentViewModel( + documentId = get().provide(), + skontoInvoiceHighlights = highlights.toList(), + skontoDocumentPagesNetworkService = get(), + skontoDocumentLayoutNetworkService = get(), + skontoFileNetworkService = get(), + skontoPageImageProcessor = get(), + ) + } +} diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoicePreviewScreenColors.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoicePreviewScreenColors.kt new file mode 100644 index 0000000000..d7d89bbf97 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoicePreviewScreenColors.kt @@ -0,0 +1,46 @@ +package net.gini.android.bank.sdk.capture.skonto.invoice + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import net.gini.android.capture.ui.theme.colors.GiniColorPrimitives + +@Immutable +data class SkontoInvoicePreviewScreenColors( + val background: Color, + val closeButton: CloseButton, +) { + + data class CloseButton( + val contentColor: Color, + val backgroundColor: Color, + ) { + companion object { + + @Composable + fun colors( + // IMPORTANT! Use GiniColorPrimitives carefully! + // Using of this class skips adaptation to light/dark modes! + contentColor: Color = GiniColorPrimitives().dark02, + // IMPORTANT! Use GiniColorPrimitives carefully! + // Using of this class skips adaptation to light/dark modes! + backgroundColor: Color = GiniColorPrimitives().light01, + ) = CloseButton( + contentColor = contentColor, + backgroundColor = backgroundColor, + ) + } + } + + companion object { + + @Composable + fun colors( + // IMPORTANT! Use GiniColorPrimitives carefully! Using of this class skips adaptation to light/dark modes! + background: Color = GiniColorPrimitives().dark01, + ) = SkontoInvoicePreviewScreenColors( + background = background, + closeButton = CloseButton.colors(), + ) + } +} diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceScreen.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceScreen.kt new file mode 100644 index 0000000000..5771102242 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/SkontoInvoiceScreen.kt @@ -0,0 +1,159 @@ +package net.gini.android.bank.sdk.capture.skonto.invoice + +import android.graphics.Bitmap +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.calculateCentroid +import androidx.compose.foundation.gestures.calculateCentroidSize +import androidx.compose.foundation.gestures.calculatePan +import androidx.compose.foundation.gestures.calculateRotation +import androidx.compose.foundation.gestures.calculateZoom +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChanged +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import net.gini.android.capture.ui.components.list.ZoomableLazyColumn +import net.gini.android.capture.ui.theme.GiniTheme +import kotlin.math.abs + +@Composable +internal fun SkontoInvoiceScreen( + navigateBack: () -> Unit, + viewModel: SkontoInvoiceFragmentViewModel, + modifier: Modifier = Modifier, + colors: SkontoInvoicePreviewScreenColors = SkontoInvoicePreviewScreenColors.colors() +) { + val state by viewModel.stateFlow.collectAsState() + + Scaffold( + modifier = modifier.fillMaxSize() + + ) { paddings -> + SkontoInvoiceScreenContent( + modifier = Modifier.padding(paddings), + state = state, + onCloseClicked = navigateBack, + colors = colors, + ) + } +} + +@Composable +private fun SkontoInvoiceScreenContent( + state: SkontoInvoiceFragmentState, + onCloseClicked: () -> Unit, + modifier: Modifier = Modifier, + colors: SkontoInvoicePreviewScreenColors = SkontoInvoicePreviewScreenColors.colors(), +) { + Box( + modifier = modifier + .fillMaxSize() + .background(colors.background) + ) { + + Box( + modifier = Modifier + .padding(vertical = 24.dp, horizontal = 24.dp) + .background(colors.closeButton.backgroundColor, CircleShape) + .clickable(onClick = onCloseClicked) + .padding(8.dp), + ) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(id = net.gini.android.capture.R.drawable.gc_close), + contentDescription = null, + tint = colors.closeButton.contentColor + ) + } + + AnimatedVisibility( + modifier = Modifier.align(Alignment.Center), visible = state.isLoading + ) { + CircularProgressIndicator() + } + + AnimatedVisibility( + modifier = Modifier + .align(Alignment.Center) + .padding(top = 64.dp), + visible = !state.isLoading + ) { + ImagesList( + pages = state.images, modifier = Modifier + ) + } + } +} + +@Composable +private fun ImagesList( + pages: List, + modifier: Modifier = Modifier, +) { + ZoomableLazyColumn( + modifier = modifier, + verticalArrangement = Arrangement.Center, + ) { + items(pages) { + Image( + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 4.dp) + .fillMaxWidth(), + bitmap = it.asImageBitmap(), + contentDescription = null, + contentScale = ContentScale.FillWidth + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun SkontoInvoiceScreenContentPreview() { + GiniTheme { + SkontoInvoiceScreenContent(state = SkontoInvoiceFragmentState( + isLoading = true, images = emptyList() + ), onCloseClicked = {}) + } +} + diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/image/SkontoPageImageProcessor.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/image/SkontoPageImageProcessor.kt new file mode 100644 index 0000000000..eab0dee1a7 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/image/SkontoPageImageProcessor.kt @@ -0,0 +1,69 @@ +package net.gini.android.bank.sdk.capture.skonto.invoice.image + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import net.gini.android.bank.sdk.capture.skonto.model.SkontoInvoiceHighlightBoxes +import net.gini.android.capture.internal.network.model.DocumentLayout +import net.gini.android.capture.network.model.GiniCaptureBox +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class SkontoPageImageProcessor { + + suspend fun processImage( + image: Bitmap, + skontoPageLayout: DocumentLayout.Page, + skontoInvoiceHighlightBoxes: SkontoInvoiceHighlightBoxes, + color: Int = 0xAAFFFF00.toInt(), + ): Bitmap = suspendCoroutine { continuation -> + + val finalBitmap = image.copy(Bitmap.Config.ARGB_8888, true) + + val scaleY = image.height.toFloat() / skontoPageLayout.sizeY + val scaleX = image.width.toFloat() / skontoPageLayout.sizeX + + val canvas = Canvas(finalBitmap) + + val boxes = skontoInvoiceHighlightBoxes.getExistBoxes() + + val scaledBoxes = boxes.map { it.scale(scaleX, scaleY) } + + val scaledRectList = scaledBoxes.map { it.toRect() } + + val paint = Paint().apply { + this.color = color + } + + canvas.drawHighlightRect(scaledRectList.unionAll(), paint) + + continuation.resume(finalBitmap) + } +} + +private fun Canvas.drawHighlightRect(rect: RectF, paint: Paint) { + drawRect(rect, paint) +} + +private fun List.unionAll() = RectF( + minOf { it.left }, + minOf { it.top }, + maxOf { it.right }, + maxOf { it.bottom }, +) + +private fun GiniCaptureBox.toRect() = RectF( + left.toFloat(), + top.toFloat(), + left.toFloat() + width.toFloat(), + top.toFloat() + height.toFloat(), +) + +private fun GiniCaptureBox.scale(scaleX: Float, scaleY: Float) = GiniCaptureBox( + pageNumber, + left * scaleX, + top * scaleY, + width * scaleX, + height * scaleY, +) diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/network/SkontoDocumentLayoutNetworkService.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/network/SkontoDocumentLayoutNetworkService.kt new file mode 100644 index 0000000000..7102cfac79 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/network/SkontoDocumentLayoutNetworkService.kt @@ -0,0 +1,34 @@ +package net.gini.android.bank.sdk.capture.skonto.invoice.network + +import kotlinx.coroutines.suspendCancellableCoroutine +import net.gini.android.capture.internal.network.model.DocumentLayout +import net.gini.android.capture.network.Error +import net.gini.android.capture.network.GiniCaptureNetworkCallback +import net.gini.android.capture.network.GiniCaptureNetworkService +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +internal class SkontoDocumentLayoutNetworkService( + private val giniCaptureNetworkService: GiniCaptureNetworkService, +) { + + suspend fun getLayout(documentId: String): DocumentLayout { + return suspendCancellableCoroutine { continuation -> + giniCaptureNetworkService.getDocumentLayout(documentId, object : + GiniCaptureNetworkCallback { + + override fun failure(error: Error) { + continuation.resumeWithException(IllegalStateException(error.message)) + } + + override fun success(result: DocumentLayout) { + continuation.resume(result) + } + + override fun cancelled() { + continuation.cancel() + } + }) + } + } +} diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/network/SkontoDocumentPagesNetworkService.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/network/SkontoDocumentPagesNetworkService.kt new file mode 100644 index 0000000000..a9779ff0b1 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/network/SkontoDocumentPagesNetworkService.kt @@ -0,0 +1,34 @@ +package net.gini.android.bank.sdk.capture.skonto.invoice.network + +import kotlinx.coroutines.suspendCancellableCoroutine +import net.gini.android.capture.internal.network.model.DocumentPage +import net.gini.android.capture.network.Error +import net.gini.android.capture.network.GiniCaptureNetworkCallback +import net.gini.android.capture.network.GiniCaptureNetworkService +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +internal class SkontoDocumentPagesNetworkService( + private val giniCaptureNetworkService: GiniCaptureNetworkService, +) { + + suspend fun getDocumentPages(documentId: String): List { + return suspendCancellableCoroutine { continuation -> + giniCaptureNetworkService.getDocumentPages(documentId, object : + GiniCaptureNetworkCallback, Error> { + + override fun failure(error: Error) { + continuation.resumeWithException(IllegalStateException(error.message)) + } + + override fun success(result: List) { + continuation.resume(result) + } + + override fun cancelled() { + continuation.cancel() + } + }) + } + } +} diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/network/SkontoFileNetworkService.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/network/SkontoFileNetworkService.kt new file mode 100644 index 0000000000..e1cb8b5c08 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/invoice/network/SkontoFileNetworkService.kt @@ -0,0 +1,33 @@ +package net.gini.android.bank.sdk.capture.skonto.invoice.network + +import kotlinx.coroutines.suspendCancellableCoroutine +import net.gini.android.capture.network.Error +import net.gini.android.capture.network.GiniCaptureNetworkCallback +import net.gini.android.capture.network.GiniCaptureNetworkService +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +internal class SkontoFileNetworkService( + private val giniCaptureNetworkService: GiniCaptureNetworkService, +) { + + suspend fun getFile(url: String): ByteArray { + return suspendCancellableCoroutine { continuation -> + giniCaptureNetworkService.getFile(url, object : + GiniCaptureNetworkCallback, Error> { + + override fun failure(error: Error) { + continuation.resumeWithException(IllegalStateException(error.message)) + } + + override fun success(result: Array) { + continuation.resume(result.toByteArray()) + } + + override fun cancelled() { + continuation.cancel() + } + }) + } + } +} diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/model/SkontoInvoiceHighlightBoxes.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/model/SkontoInvoiceHighlightBoxes.kt new file mode 100644 index 0000000000..69b463105e --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/model/SkontoInvoiceHighlightBoxes.kt @@ -0,0 +1,25 @@ +package net.gini.android.bank.sdk.capture.skonto.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import net.gini.android.capture.network.model.GiniCaptureBox + +@Parcelize +data class SkontoInvoiceHighlightBoxes( + val skontoPercentageDiscounted: GiniCaptureBox?, + val skontoPaymentMethod: GiniCaptureBox?, + val skontoAmountToPay: GiniCaptureBox?, + val skontoAmountDiscounted: GiniCaptureBox?, + val skontoRemainingDays: GiniCaptureBox?, + val skontoDueDate: GiniCaptureBox?, +) : Parcelable { + + fun getExistBoxes() = listOfNotNull( + skontoPercentageDiscounted, + skontoPaymentMethod, + skontoAmountToPay, + skontoAmountDiscounted, + skontoRemainingDays, + skontoDueDate, + ) +} diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/di/BankSdkIsolatedKoinContext.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/di/BankSdkIsolatedKoinContext.kt new file mode 100644 index 0000000000..6317e19a56 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/di/BankSdkIsolatedKoinContext.kt @@ -0,0 +1,25 @@ +package net.gini.android.bank.sdk.di + +import net.gini.android.bank.sdk.capture.captureFlowFragmentModule +import net.gini.android.bank.sdk.capture.skonto.invoice.skontoInvoiceScreenModule +import net.gini.android.bank.sdk.capture.skonto.skontoScreenModule +import org.koin.dsl.koinApplication + +object BankSdkIsolatedKoinContext { + + private val koinApp = koinApplication { + modules( + screenModules + ) + } + + val koin = koinApp.koin +} + +private val screenModules = listOf( + skontoScreenModule, + skontoInvoiceScreenModule, + captureFlowFragmentModule +) + +fun getGiniBankKoin() = BankSdkIsolatedKoinContext.koin diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/di/IsolatedKoinContext.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/di/IsolatedKoinContext.kt deleted file mode 100644 index 58c43815b6..0000000000 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/di/IsolatedKoinContext.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.gini.android.bank.sdk.di - -import net.gini.android.bank.sdk.capture.skonto.skontoScreenModule -import org.koin.dsl.koinApplication - -object IsolatedKoinContext { - - private val koinApp = koinApplication { - modules(skontoScreenModule) - } - - val koin = koinApp.koin -} - -fun getGiniKoin() = IsolatedKoinContext.koin \ No newline at end of file diff --git a/bank-sdk/sdk/src/main/res/navigation/gbs_nav_graph.xml b/bank-sdk/sdk/src/main/res/navigation/gbs_nav_graph.xml index 070dfa13aa..7c3112c039 100644 --- a/bank-sdk/sdk/src/main/res/navigation/gbs_nav_graph.xml +++ b/bank-sdk/sdk/src/main/res/navigation/gbs_nav_graph.xml @@ -36,7 +36,7 @@ - + - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/digitalinvoice/details/LineItemDetailsScreenPresenterTest.kt b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/digitalinvoice/details/LineResponseItemDetailsScreenPresenterTest.kt similarity index 99% rename from bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/digitalinvoice/details/LineItemDetailsScreenPresenterTest.kt rename to bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/digitalinvoice/details/LineResponseItemDetailsScreenPresenterTest.kt index e449c78739..1ea12d962e 100644 --- a/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/digitalinvoice/details/LineItemDetailsScreenPresenterTest.kt +++ b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/digitalinvoice/details/LineResponseItemDetailsScreenPresenterTest.kt @@ -20,7 +20,7 @@ import java.util.* @RunWith(JUnitParamsRunner::class) -class LineItemDetailsScreenPresenterTest { +class LineResponseItemDetailsScreenPresenterTest { private val returnReasonsFixture = listOf( GiniCaptureReturnReason("1", mapOf("de" to "Foo", "en" to "Foo")), diff --git a/capture-sdk/default-network/src/main/java/net/gini/android/capture/network/GiniCaptureDefaultNetworkService.kt b/capture-sdk/default-network/src/main/java/net/gini/android/capture/network/GiniCaptureDefaultNetworkService.kt index 2c2256f4e3..43a600e409 100644 --- a/capture-sdk/default-network/src/main/java/net/gini/android/capture/network/GiniCaptureDefaultNetworkService.kt +++ b/capture-sdk/default-network/src/main/java/net/gini/android/capture/network/GiniCaptureDefaultNetworkService.kt @@ -15,6 +15,8 @@ import net.gini.android.capture.GiniCapture import net.gini.android.capture.document.GiniCaptureMultiPageDocument import net.gini.android.capture.internal.network.AmplitudeRoot import net.gini.android.capture.internal.network.Configuration +import net.gini.android.capture.internal.network.model.DocumentLayout +import net.gini.android.capture.internal.network.model.DocumentPage import net.gini.android.capture.logging.ErrorLog import net.gini.android.capture.network.GiniCaptureDefaultNetworkService.Companion.builder import net.gini.android.capture.network.logging.formattedErrorMessage @@ -369,6 +371,109 @@ class GiniCaptureDefaultNetworkService( } } + override fun getDocumentLayout( + documentId: String, + callback: GiniCaptureNetworkCallback + ): CancellationToken { + + LOG.debug("Getting layout for document {}", documentId) + + return launchCancellable { + + when (val resource = giniBankApi.documentManager.getLayoutModel(documentId)) { + + is Resource.Cancelled -> { + LOG.debug("Getting layout for document {} canceled", documentId) + } + + is Resource.Error -> { + val errorMessage = Error(resource.formattedErrorMessage) + LOG.error( + "Getting layout for document {} failed. {}", + documentId, + errorMessage + ) + callback.failure(errorMessage) + } + + is Resource.Success -> { + LOG.debug( + "Getting layout for document {} success.\n{}", + documentId, + resource.data + ) + callback.success(resource.data.toCaptureDocumentLayout()) + } + } + } + } + + override fun getDocumentPages( + documentId: String, + callback: GiniCaptureNetworkCallback, Error> + ): CancellationToken { + + return launchCancellable { + when (val resource = giniBankApi.documentManager.getDocumentPages(documentId)) { + is Resource.Cancelled -> { + LOG.debug("Getting pages for document {} canceled", documentId) + } + + is Resource.Error -> { + val errorMessage = Error(resource.formattedErrorMessage) + LOG.error( + "Getting pages for document {} failed. {}", + documentId, + errorMessage + ) + callback.failure(errorMessage) + } + + is Resource.Success -> { + LOG.debug( + "Getting pages for document {} success. {}", + documentId, + resource.data.toString() + ) + callback.success(resource.data.map { it.toCaptureDocumentPages() }) + } + } + } + } + + override fun getFile( + fileUrl: String, + callback: GiniCaptureNetworkCallback, Error> + ): CancellationToken { + + return launchCancellable { + when (val resource = giniBankApi.documentManager.getFile(fileUrl)) { + is Resource.Cancelled -> { + LOG.debug("Getting file for document {} canceled", fileUrl) + } + + is Resource.Error -> { + val errorMessage = Error(resource.formattedErrorMessage) + LOG.error( + "Getting file for document {} failed. {}", + fileUrl, + errorMessage + ) + callback.failure(errorMessage) + } + + is Resource.Success -> { + LOG.debug( + "Getting file for document {} success. ByteArray size: {}", + fileUrl, + resource.data.size + ) + callback.success(resource.data.toTypedArray()) + } + } + } + } + override fun sendFeedback( extractions: MutableMap, compoundExtractions: MutableMap, diff --git a/capture-sdk/default-network/src/main/java/net/gini/android/capture/network/model/DocumentLayoutMapper.kt b/capture-sdk/default-network/src/main/java/net/gini/android/capture/network/model/DocumentLayoutMapper.kt new file mode 100644 index 0000000000..f3b318966f --- /dev/null +++ b/capture-sdk/default-network/src/main/java/net/gini/android/capture/network/model/DocumentLayoutMapper.kt @@ -0,0 +1,65 @@ +package net.gini.android.capture.network.model + +import net.gini.android.core.api.models.DocumentLayout +import net.gini.android.capture.internal.network.model.DocumentLayout as CaptureApiDocumentLayout +import net.gini.android.capture.internal.network.model.DocumentLayout.Page.Region as CaptureApiRegion +import net.gini.android.capture.internal.network.model.DocumentLayout.Page.TextZone.Paragraph as CaptureApiParagraph +import net.gini.android.capture.internal.network.model.DocumentLayout.Page.TextZone.Paragraph.Line as CaptureApiLine +import net.gini.android.capture.internal.network.model.DocumentLayout.Page.TextZone.Paragraph.Line.Word as CaptureApiWord +import net.gini.android.capture.internal.network.model.DocumentLayout.Page as CaptureApiPage +import net.gini.android.capture.internal.network.model.DocumentLayout.Page.TextZone as CaptureApiTextZone + + +fun DocumentLayout.toCaptureDocumentLayout() = CaptureApiDocumentLayout( + pages = pages.map { it.toPage() } +) + +fun DocumentLayout.Page.toPage() = CaptureApiPage( + number = number, + sizeX = sizeX, + sizeY = sizeY, + textZones = textZones.map { it.toTextZone() }, + regions = regions.map { it.toRegion() } +) + +fun DocumentLayout.Page.TextZone.toTextZone() = CaptureApiTextZone( + paragraphs = paragraphs.map { it.toParagraph() } +) + +fun DocumentLayout.Page.TextZone.Paragraph.toParagraph() = + CaptureApiParagraph( + left = left, + top = top, + width = width, + height = height, + lines = lines.map { it.toLine() }) + +fun DocumentLayout.Page.TextZone.Paragraph.Line.toLine() = + CaptureApiLine( + words = words.map { it.toWord() }, + top = top, + left = left, + width = width, + height = height, + ) + +fun DocumentLayout.Page.TextZone.Paragraph.Line.Word.toWord() = + CaptureApiWord( + text = text, + left = left, + top = top, + width = width, + height = height, + fontSize = fontSize, + bold = bold, + fontFamily = fontFamily + ) + +fun DocumentLayout.Page.Region.toRegion() = + CaptureApiRegion( + width = width, + height = height, + left = left, + top = top, + type = type + ) \ No newline at end of file diff --git a/capture-sdk/default-network/src/main/java/net/gini/android/capture/network/model/DocumentPagesMapper.kt b/capture-sdk/default-network/src/main/java/net/gini/android/capture/network/model/DocumentPagesMapper.kt new file mode 100644 index 0000000000..2416f28210 --- /dev/null +++ b/capture-sdk/default-network/src/main/java/net/gini/android/capture/network/model/DocumentPagesMapper.kt @@ -0,0 +1,16 @@ +package net.gini.android.capture.network.model + +import net.gini.android.core.api.models.DocumentPage + + +fun DocumentPage.toCaptureDocumentPages() = + net.gini.android.capture.internal.network.model.DocumentPage( + pageNumber = pageNumber, + images = images.toCaptureImage(), + ) + +fun DocumentPage.Images.toCaptureImage() = + net.gini.android.capture.internal.network.model.DocumentPage.Images( + medium = medium, + large = large, + ) diff --git a/capture-sdk/sdk/build.gradle.kts b/capture-sdk/sdk/build.gradle.kts index 798801c948..682be37edc 100644 --- a/capture-sdk/sdk/build.gradle.kts +++ b/capture-sdk/sdk/build.gradle.kts @@ -145,6 +145,12 @@ dependencies { implementation(libs.navigation.fragment.ktx) implementation(libs.navigation.ui.ktx) + implementation(platform(libs.koin.bom)) + implementation(libs.koin.core) + implementation(libs.koin.android) + implementation(libs.koin.androidx.compose) + implementation(libs.koin.android.compat) + implementation(platform(libs.compose.bom)) implementation(libs.compose.material3) implementation(libs.compose.tools.uiToolingPreview) diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/GiniCaptureFragment.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/GiniCaptureFragment.kt index 55e36ec00d..f2226c7dd1 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/GiniCaptureFragment.kt +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/GiniCaptureFragment.kt @@ -337,7 +337,9 @@ class CaptureFragmentFactory( } interface GiniCaptureFragmentListener { - fun onFinishedWithResult(result: CaptureSDKResult) + fun onFinishedWithResult( + result: CaptureSDKResult + ) fun onCheckImportedDocument( document: Document, diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/AnalysisInteractor.java b/capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/AnalysisInteractor.java index ea7b075d02..e169480ca6 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/AnalysisInteractor.java +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/AnalysisInteractor.java @@ -23,11 +23,12 @@ import java.util.Map; import androidx.annotation.NonNull; + import jersey.repackaged.jsr166e.CompletableFuture; /** * Created by Alpar Szotyori on 09.05.2019. - * + *

* Copyright (c) 2019 Gini GmbH. */ @@ -77,12 +78,13 @@ public ResultHolder apply( final Map compoundExtractions = requestResult.getAnalysisResult().getCompoundExtractions(); if (extractions.isEmpty() && compoundExtractions.isEmpty()) { - return new ResultHolder(Result.SUCCESS_NO_EXTRACTIONS); + return new ResultHolder(Result.SUCCESS_NO_EXTRACTIONS, requestResult.getApiDocumentId()); } else { return new ResultHolder(Result.SUCCESS_WITH_EXTRACTIONS, extractions, compoundExtractions, - requestResult.getAnalysisResult().getReturnReasons()); + requestResult.getAnalysisResult().getReturnReasons(), + requestResult.getApiDocumentId()); } } return null; @@ -90,10 +92,10 @@ public ResultHolder apply( }); } else { return CompletableFuture.completedFuture( - new ResultHolder(Result.NO_NETWORK_SERVICE)); + new ResultHolder(Result.NO_NETWORK_SERVICE, null)); } } else { - return CompletableFuture.completedFuture(new ResultHolder(Result.NO_NETWORK_SERVICE)); + return CompletableFuture.completedFuture(new ResultHolder(Result.NO_NETWORK_SERVICE, null)); } } @@ -170,22 +172,26 @@ public static final class ResultHolder { private final Map mExtractions; private final Map mCompoundExtractions; private final List mReturnReasons; + private final String mDocumentId; - ResultHolder(@NonNull final Result result) { - this(result, Collections.emptyMap(), - Collections.emptyMap(), - Collections.emptyList()); + ResultHolder(@NonNull final Result result, final String documentId) { + this(result, Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyList(), + documentId); } ResultHolder( @NonNull final Result result, @NonNull final Map extractions, @NonNull final Map compoundExtractions, - @NonNull final List returnReasons) { + @NonNull final List returnReasons, + final String documentId) { mResult = result; mExtractions = extractions; mCompoundExtractions = compoundExtractions; mReturnReasons = returnReasons; + mDocumentId = documentId; } @NonNull @@ -207,5 +213,9 @@ public Map getCompoundExtractions() { public List getReturnReasons() { return mReturnReasons; } + + public String getDocumentId() { + return mDocumentId; + } } } diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/AnalysisScreenPresenter.java b/capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/AnalysisScreenPresenter.java index d87bf53280..ee01d2e8da 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/AnalysisScreenPresenter.java +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/AnalysisScreenPresenter.java @@ -11,6 +11,7 @@ import net.gini.android.capture.Document; import net.gini.android.capture.GiniCapture; import net.gini.android.capture.GiniCaptureError; +import net.gini.android.capture.di.CaptureSdkIsolatedKoinContextKt; import net.gini.android.capture.document.DocumentFactory; import net.gini.android.capture.document.GiniCaptureDocument; import net.gini.android.capture.document.GiniCaptureDocumentError; @@ -42,6 +43,7 @@ import java.util.Random; import jersey.repackaged.jsr166e.CompletableFuture; +import kotlin.Lazy; import static net.gini.android.capture.GiniCaptureError.ErrorCode.MISSING_GINI_CAPTURE_INSTANCE; import static net.gini.android.capture.internal.util.NullabilityHelper.getListOrEmpty; @@ -62,6 +64,8 @@ class AnalysisScreenPresenter extends AnalysisScreenContract.Presenter { @VisibleForTesting static final String PARCELABLE_MEMORY_CACHE_TAG = "ANALYSIS_FRAGMENT"; private static final Logger LOG = LoggerFactory.getLogger(AnalysisScreenPresenter.class); + private final AnalysisScreenPresenterExtension analysisScreenPresenterExtension; + private static final AnalysisFragmentListener NO_OP_LISTENER = new AnalysisFragmentListener() { @Override public void onError(@NonNull final GiniCaptureError error) { @@ -118,6 +122,7 @@ public void onDefaultPDFAppAlertDialogCancelled() { mDocumentAnalysisErrorMessage = documentAnalysisErrorMessage; mAnalysisInteractor = analysisInteractor; mHints = generateRandomHintsList(); + analysisScreenPresenterExtension = new AnalysisScreenPresenterExtension(); } private List generateRandomHintsList() { @@ -301,12 +306,16 @@ public Void apply(final AnalysisInteractor.ResultHolder resultHolder, switch (result) { case SUCCESS_NO_EXTRACTIONS: mAnalysisCompleted = true; + analysisScreenPresenterExtension.getLastAnalyzedDocumentIdProvider() + .update(resultHolder.getDocumentId()); trackAnalysisScreenEvent(AnalysisScreenEvent.NO_RESULTS); getAnalysisFragmentListenerOrNoOp() .onProceedToNoExtractionsScreen(mMultiPageDocument); break; case SUCCESS_WITH_EXTRACTIONS: mAnalysisCompleted = true; + analysisScreenPresenterExtension.getLastAnalyzedDocumentIdProvider() + .update(resultHolder.getDocumentId()); if (resultHolder.getExtractions().isEmpty()) { trackAnalysisScreenEvent(AnalysisScreenEvent.NO_RESULTS); getAnalysisFragmentListenerOrNoOp() diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/AnalysisScreenPresenterExtension.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/AnalysisScreenPresenterExtension.kt new file mode 100644 index 0000000000..b52231cbca --- /dev/null +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/AnalysisScreenPresenterExtension.kt @@ -0,0 +1,10 @@ +package net.gini.android.capture.analysis + +import net.gini.android.capture.di.getGiniCaptureKoin + +open class AnalysisScreenPresenterExtension { + + val lastAnalyzedDocumentIdProvider: LastAnalyzedDocumentIdProvider by + getGiniCaptureKoin().inject() + +} diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/LastAnalyzedDocumentIdProvider.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/LastAnalyzedDocumentIdProvider.kt new file mode 100644 index 0000000000..80d3bbc934 --- /dev/null +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/analysis/LastAnalyzedDocumentIdProvider.kt @@ -0,0 +1,16 @@ +package net.gini.android.capture.analysis + +class LastAnalyzedDocumentIdProvider { + + private var lastAnalyzedDocumentId: String? = null + + fun provide(): String? = lastAnalyzedDocumentId + + fun update(documentId: String) { + lastAnalyzedDocumentId = documentId + } + + fun clear() { + lastAnalyzedDocumentId = null + } +} diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/di/CaptureSdkIsolatedKoinContext.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/di/CaptureSdkIsolatedKoinContext.kt new file mode 100644 index 0000000000..192fe8f547 --- /dev/null +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/di/CaptureSdkIsolatedKoinContext.kt @@ -0,0 +1,14 @@ +package net.gini.android.capture.di + +import org.koin.dsl.koinApplication + +object CaptureSdkIsolatedKoinContext { + + private val koinApp = koinApplication { + modules(providerModule) + } + + val koin = koinApp.koin +} + +fun getGiniCaptureKoin() = CaptureSdkIsolatedKoinContext.koin diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/di/ProviderModule.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/di/ProviderModule.kt new file mode 100644 index 0000000000..5d86fdd65b --- /dev/null +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/di/ProviderModule.kt @@ -0,0 +1,8 @@ +package net.gini.android.capture.di + +import net.gini.android.capture.analysis.LastAnalyzedDocumentIdProvider +import org.koin.dsl.module + +internal val providerModule = module { + single { LastAnalyzedDocumentIdProvider() } +} diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/model/DocumentLayout.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/model/DocumentLayout.kt new file mode 100644 index 0000000000..63c01de8bb --- /dev/null +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/model/DocumentLayout.kt @@ -0,0 +1,57 @@ +package net.gini.android.capture.internal.network.model + +data class DocumentLayout( + val pages: List +) { + + data class Page( + val number: Int, + val sizeX: Float, + val sizeY: Float, + val textZones: List, + val regions: List + ) { + + data class TextZone( + val paragraphs: List, + ) { + + data class Paragraph( + val width: Float, + val height: Float, + val top: Float, + val left: Float, + val lines: List, + ) { + + data class Line( + val width: Float, + val height: Float, + val top: Float, + val left: Float, + val words: List, + ) { + + data class Word( + val width: Float, + val height: Float, + val top: Float, + val left: Float, + val fontSize: Float, + val fontFamily: String, + val bold: Boolean, + val text: String, + ) + } + } + } + + data class Region( + val width: Float, + val height: Float, + val top: Float, + val left: Float, + val type: String, + ) + } +} diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/model/DocumentPage.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/model/DocumentPage.kt new file mode 100644 index 0000000000..8d221a665c --- /dev/null +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/model/DocumentPage.kt @@ -0,0 +1,13 @@ +package net.gini.android.capture.internal.network.model + +data class DocumentPage( + val pageNumber: Int, + val images: Images +) { + data class Images( + val medium: String?, + val large: String?, + ) + + fun getSmallestImage() = images.medium ?: images.large +} diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/network/GiniCaptureNetworkService.java b/capture-sdk/sdk/src/main/java/net/gini/android/capture/network/GiniCaptureNetworkService.java index bed7c267e8..52ffe2d0eb 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/network/GiniCaptureNetworkService.java +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/network/GiniCaptureNetworkService.java @@ -8,6 +8,8 @@ import net.gini.android.capture.GiniCapture; import net.gini.android.capture.internal.network.AmplitudeRoot; import net.gini.android.capture.internal.network.Configuration; +import net.gini.android.capture.internal.network.model.DocumentLayout; +import net.gini.android.capture.internal.network.model.DocumentPage; import net.gini.android.capture.logging.ErrorLog; import net.gini.android.capture.logging.ErrorLoggerListener; import net.gini.android.capture.network.model.GiniCaptureCompoundExtraction; @@ -15,11 +17,12 @@ import net.gini.android.capture.util.CancellationToken; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; /** * Created by Alpar Szotyori on 29.01.2018. - * + *

* Copyright (c) 2018 Gini GmbH. */ @@ -37,7 +40,6 @@ *

In order for the Gini Capture SDK to use your implementation pass an instance of it to * {@link GiniCapture.Builder#setGiniCaptureNetworkService(GiniCaptureNetworkService)} when creating a * {@link GiniCapture} instance. - * */ public interface GiniCaptureNetworkService extends ErrorLoggerListener { @@ -49,22 +51,20 @@ public interface GiniCaptureNetworkService extends ErrorLoggerListener { * * @param document a {@link Document} containing an image, pdf or other supported formats * @param callback a callback implementation to return the outcome of the upload - * * @return a {@link CancellationToken} to be used for requesting upload cancellation */ CancellationToken upload(@NonNull final Document document, - @NonNull final GiniCaptureNetworkCallback callback); + @NonNull final GiniCaptureNetworkCallback callback); /** * Called when a document needs to be deleted from the Gini API. * * @param giniApiDocumentId id of the document received when it was uploaded to the Gini API * @param callback a callback implementation to return the outcome of the deletion - * * @return a {@link CancellationToken} to be used for requesting cancellation of the deletion */ CancellationToken delete(@NonNull final String giniApiDocumentId, - @NonNull final GiniCaptureNetworkCallback callback); + @NonNull final GiniCaptureNetworkCallback callback); /** * Called when a document needs to be analyzed by the Gini API. @@ -76,15 +76,12 @@ CancellationToken delete(@NonNull final String giniApiDocumentId, * document rotations * @param callback a callback implementation to return the outcome of the * analysis - * * @return a {@link CancellationToken} to be used for requesting analysis cancellation */ CancellationToken analyze( @NonNull final LinkedHashMap giniApiDocumentIdRotationMap, // NOPMD @NonNull final GiniCaptureNetworkCallback callback); - - /** * Call this method with the extractions the user has seen and accepted. The {@link * GiniCaptureSpecificExtraction}s must contain the final user corrected and/or accepted values. @@ -115,6 +112,23 @@ default CancellationToken getConfiguration(@NonNull final GiniCaptureNetworkCall return null; } + default CancellationToken getDocumentLayout( + @NonNull final String documentId, + @NonNull final GiniCaptureNetworkCallback callback + ) { + return null; + } + + default CancellationToken getDocumentPages(@NonNull final String documentId, + @NonNull final GiniCaptureNetworkCallback, Error> callback) { + return null; + } + + default CancellationToken getFile(@NonNull final String fileUrl, + @NonNull final GiniCaptureNetworkCallback callback) { + return null; + } + default CancellationToken sendEvents(@NonNull final AmplitudeRoot amplitudeRoot, @NonNull final GiniCaptureNetworkCallback callback) { return null; } diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/button/filled/GiniButton.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/button/filled/GiniButton.kt index 427b66fbd6..02b835e6a6 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/button/filled/GiniButton.kt +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/button/filled/GiniButton.kt @@ -31,7 +31,9 @@ fun GiniButton( modifier = modifier, giniButtonColors = giniButtonColors, ) { - Text(text = text) + Text( + text = text, + ) } } diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/list/ZoomableLazyColumn.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/list/ZoomableLazyColumn.kt new file mode 100644 index 0000000000..2d0668fd23 --- /dev/null +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/list/ZoomableLazyColumn.kt @@ -0,0 +1,211 @@ +package net.gini.android.capture.ui.components.list + +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.ScrollableDefaults +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.calculateCentroid +import androidx.compose.foundation.gestures.calculateCentroidSize +import androidx.compose.foundation.gestures.calculatePan +import androidx.compose.foundation.gestures.calculateRotation +import androidx.compose.foundation.gestures.calculateZoom +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChanged +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import kotlin.math.abs + +@Composable +fun ZoomableLazyColumn( + modifier: Modifier = Modifier, + minScale: Float = 1f, + maxScale: Float = 3f, + state: LazyListState = rememberLazyListState(), + contentPadding: PaddingValues = PaddingValues(0.dp), + reverseLayout: Boolean = false, + verticalArrangement: Arrangement.Vertical = + if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), + userScrollEnabled: Boolean = true, + content: LazyListScope.() -> Unit +) { + + var scale by remember { mutableFloatStateOf(minScale) } + var offsetX by remember { mutableFloatStateOf(0f) } + var offsetY by remember { mutableFloatStateOf(0f) } + var size by remember { mutableStateOf(IntSize.Zero) } + + LazyColumn( + contentPadding = contentPadding, + modifier = modifier + .fillMaxSize() + .onSizeChanged { size = it } + .pointerInput(Unit) { + detectTransformGestures( + onGesture = { _, pan, zoom, _, _, _ -> + scale = maxOf(minScale, minOf(scale * zoom, maxScale)) + val maxX = (size.width * (scale - 1)) / 2 + val minX = -maxX + offsetX = maxOf(minX, minOf(maxX, offsetX + pan.x)) + val maxY = (size.height * (scale - 1)) / 2 + val minY = -maxY + offsetY = maxOf(minY, minOf(maxY, offsetY + pan.y)) + }, + onGestureEnd = { + + }, + onGestureStart = { + + } + ) + } + .graphicsLayer( + scaleX = scale, + scaleY = scale, + translationX = offsetX, + translationY = offsetY, + ), + state = state, + verticalArrangement = verticalArrangement, + horizontalAlignment = horizontalAlignment, + reverseLayout = reverseLayout, + flingBehavior = flingBehavior, + userScrollEnabled = userScrollEnabled + ) { + content() + } +} + +@Suppress("CyclomaticComplexMethod", "LongMethod", "MagicNumber") +private suspend fun PointerInputScope.detectTransformGestures( + panZoomLock: Boolean = false, + consume: Boolean = true, + pass: PointerEventPass = PointerEventPass.Main, + onGestureStart: (PointerInputChange) -> Unit = {}, + onGesture: ( + centroid: Offset, + pan: Offset, + zoom: Float, + rotation: Float, + mainPointer: PointerInputChange, + changes: List + ) -> Unit, + onGestureEnd: (PointerInputChange) -> Unit = {} +) { + awaitEachGesture { + var rotation = 0f + var zoom = 1f + var pan = Offset.Zero + var pastTouchSlop = false + val touchSlop = viewConfiguration.touchSlop + var lockedToPanZoom = false + + // Wait for at least one pointer to press down, and set first contact position + val down: PointerInputChange = awaitFirstDown( + requireUnconsumed = false, + pass = pass + ) + onGestureStart(down) + + var pointer = down + // Main pointer is the one that is down initially + var pointerId = down.id + + do { + val event = awaitPointerEvent(pass = pass) + + // If any position change is consumed from another PointerInputChange + // or pointer count requirement is not fulfilled + val canceled = + event.changes.any { it.isConsumed } + + if (!canceled) { + + // Get pointer that is down, if first pointer is up + // get another and use it if other pointers are also down + // event.changes.first() doesn't return same order + val pointerInputChange = + event.changes.firstOrNull { it.id == pointerId } + ?: event.changes.first() + + // Next time will check same pointer with this id + pointerId = pointerInputChange.id + pointer = pointerInputChange + + val zoomChange = event.calculateZoom() + val rotationChange = event.calculateRotation() + val panChange = event.calculatePan() + + if (!pastTouchSlop) { + zoom *= zoomChange + rotation += rotationChange + pan += panChange + + val centroidSize = event.calculateCentroidSize(useCurrent = false) + val zoomMotion = abs(1 - zoom) * centroidSize + val rotationMotion = + abs(rotation * kotlin.math.PI.toFloat() * centroidSize / 180f) + val panMotion = pan.getDistance() + + if (zoomMotion > touchSlop || + rotationMotion > touchSlop || + panMotion > touchSlop + ) { + pastTouchSlop = true + lockedToPanZoom = panZoomLock && rotationMotion < touchSlop + } + } + + if (pastTouchSlop) { + val centroid = event.calculateCentroid(useCurrent = false) + val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange + if (effectiveRotation != 0f || + zoomChange != 1f || + panChange != Offset.Zero + ) { + onGesture( + centroid, + panChange, + zoomChange, + effectiveRotation, + pointer, + event.changes + ) + } + + if (consume) { + event.changes.forEach { + if (it.positionChanged()) { + it.consume() + } + } + } + } + } + } while (!canceled && event.changes.any { it.pressed }) + onGestureEnd(pointer) + } +} + diff --git a/capture-sdk/sdk/src/test/java/net/gini/android/capture/analysis/AnalysisScreenPresenterTest.kt b/capture-sdk/sdk/src/test/java/net/gini/android/capture/analysis/AnalysisScreenPresenterTest.kt index 3bc495621b..903aa8c27f 100644 --- a/capture-sdk/sdk/src/test/java/net/gini/android/capture/analysis/AnalysisScreenPresenterTest.kt +++ b/capture-sdk/sdk/src/test/java/net/gini/android/capture/analysis/AnalysisScreenPresenterTest.kt @@ -478,7 +478,7 @@ class AnalysisScreenPresenterTest { // Then verify(listener) - .onExtractionsAvailable(extractions, compoundExtraction, returnReasons) + .onExtractionsAvailable(imageDocument, extractions, compoundExtraction, returnReasons) } @Test diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 9b4a4ed993..a7e686764a 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -129,6 +129,7 @@ complexity: LongMethod: active: true threshold: 60 + ignoreAnnotated: [ 'Composable' ] LongParameterList: active: true functionThreshold: 6 @@ -136,6 +137,7 @@ complexity: ignoreDefaultParameters: true ignoreDataClasses: true ignoreAnnotatedParameter: [ ] + ignoreAnnotated: [ 'Composable' ] MethodOverloading: active: false threshold: 6 @@ -741,7 +743,7 @@ style: UnusedPrivateMember: active: true allowedNames: '' - ignoreAnnotated: [ 'Preview ' ] + ignoreAnnotated: [ 'Preview' ] UnusedPrivateProperty: active: true allowedNames: '_|ignored|expected|serialVersionUID' diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentManager.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentManager.kt index 016eb0bcd0..676e34f3d1 100644 --- a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentManager.kt +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentManager.kt @@ -3,6 +3,8 @@ package net.gini.android.core.api import android.net.Uri import net.gini.android.core.api.models.CompoundExtraction import net.gini.android.core.api.models.Document +import net.gini.android.core.api.models.DocumentLayout +import net.gini.android.core.api.models.DocumentPage import net.gini.android.core.api.models.ExtractionsContainer import net.gini.android.core.api.models.Payment import net.gini.android.core.api.models.PaymentRequest @@ -134,11 +136,45 @@ abstract class DocumentManager, E: ExtractionsCont * @param document The document for which the layouts is requested. * @return [Resource] with a [JSONObject] instance containing the layout or information about the error */ + @Deprecated( + "This method is deprecated and can be deleted in future. Use another one, please.", + replaceWith = ReplaceWith("getLayoutModel(documentId)") + ) suspend fun getLayout( document: Document ): Resource = documentRepository.getLayout(document) + /** + * Gets the pages of a document. + * + * @param documentId The document ID for which the pages are requested. + * @return [Resource] with a list of [DocumentPage] instance containing the pages or information about the error + */ + suspend fun getDocumentPages(documentId: String) : Resource> = + documentRepository.getDocumentPages(documentId) + + + /** + * Gets the file of a document. + * + * @param location The location of the file. + * @return [Resource] with a byte array containing the file or information about the error + */ + + suspend fun getFile(location: String): Resource = + documentRepository.getFile(location) + + /** + * Gets the layout of a document. The layout of the document describes the textual content of a document with + * positional information, based on the processed document. + * + * @param documentId The document ID for which the layouts is requested. + * @return [Resource] with a [DocumentLayout] instance containing the layout or information about the error + */ + suspend fun getLayoutModel(documentId: String) : Resource = + documentRepository.getLayoutModel(documentId) + /** * Get all extractions (specific and compound) for the given document. * diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRemoteSource.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRemoteSource.kt index 9c9f867379..745d2a13dd 100644 --- a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRemoteSource.kt +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRemoteSource.kt @@ -2,7 +2,10 @@ package net.gini.android.core.api import android.net.Uri import kotlinx.coroutines.withContext -import net.gini.android.core.api.authorization.apimodels.SessionToken +import net.gini.android.core.api.mapper.toDocumentLayout +import net.gini.android.core.api.mapper.toDocumentPage +import net.gini.android.core.api.models.DocumentLayout +import net.gini.android.core.api.models.DocumentPage import net.gini.android.core.api.models.Payment import net.gini.android.core.api.models.toPayment import net.gini.android.core.api.requests.ApiException @@ -72,6 +75,10 @@ abstract class DocumentRemoteSource( response.body()?.string() ?: throw ApiException.forResponse("Empty response body", response) } + @Deprecated( + "This method is deprecated and can be deleted in future. Use another one, please.", + replaceWith = ReplaceWith("getLayoutModel(accessToken, documentId)") + ) suspend fun getLayout(accessToken: String, documentId: String): String = withContext(coroutineContext) { val response = SafeApiRequest.apiRequest { documentService.getLayoutForDocument(bearerHeaderMap(accessToken, contentType = giniApiType.giniJsonMediaType), documentId) @@ -79,6 +86,25 @@ abstract class DocumentRemoteSource( response.body()?.string() ?: throw ApiException.forResponse("Empty response body", response) } + suspend fun getLayoutModel(accessToken: String, documentId: String): DocumentLayout = + withContext(coroutineContext) { + val response = SafeApiRequest.apiRequest { + documentService.getLayoutModel( + bearerHeaderMap(accessToken, contentType = giniApiType.giniJsonMediaType), documentId + ) + } + response.body()?.toDocumentLayout() ?: throw ApiException.forResponse("Empty response body", response) + } + + suspend fun getDocumentPages(accessToken: String, documentId: String): List = withContext(coroutineContext) { + val response = SafeApiRequest.apiRequest { + documentService.getDocumentPages( + bearerHeaderMap(accessToken, contentType = giniApiType.giniJsonMediaType), documentId + ) + } + response.body()?.map { it.toDocumentPage() } ?: throw ApiException.forResponse("Empty response body", response) + } + suspend fun sendFeedback(accessToken: String, documentId: String, requestBody: RequestBody): Unit = withContext(coroutineContext) { SafeApiRequest.apiRequest { documentService.sendFeedback(bearerHeaderMap(accessToken, contentType = giniApiType.giniJsonMediaType), documentId, requestBody) diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRepository.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRepository.kt index afe5d9e46c..03789e19c1 100644 --- a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRepository.kt +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRepository.kt @@ -9,6 +9,8 @@ import net.gini.android.core.api.authorization.SessionManager import net.gini.android.core.api.models.Box import net.gini.android.core.api.models.CompoundExtraction import net.gini.android.core.api.models.Document +import net.gini.android.core.api.models.DocumentLayout +import net.gini.android.core.api.models.DocumentPage import net.gini.android.core.api.models.Extraction import net.gini.android.core.api.models.ExtractionsContainer import net.gini.android.core.api.models.Payment @@ -230,6 +232,8 @@ abstract class DocumentRepository( } } + @Deprecated( "This method is deprecated and can be deleted in future. Use another one, please.", + replaceWith = ReplaceWith("getLayoutModel(documentId)")) suspend fun getLayout(document: Document): Resource { return withAccessToken { accessToken -> wrapInResource { @@ -239,6 +243,22 @@ abstract class DocumentRepository( } } + suspend fun getLayoutModel(documentId: String) : Resource { + return withAccessToken { accessToken -> + wrapInResource { + documentRemoteSource.getLayoutModel(accessToken, documentId) + } + } + } + + suspend fun getDocumentPages(documentId: String) : Resource> { + return withAccessToken { accessToken -> + wrapInResource { + documentRemoteSource.getDocumentPages(accessToken, documentId) + } + } + } + suspend fun getFile(location: String): Resource = withAccessToken { accessToken -> wrapInResource { diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentService.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentService.kt index 69bd21b0a6..24b9a1c66f 100644 --- a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentService.kt +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentService.kt @@ -1,8 +1,8 @@ package net.gini.android.core.api import android.net.Uri -import net.gini.android.core.api.authorization.UserService -import net.gini.android.core.api.models.Document +import net.gini.android.core.api.response.DocumentLayoutResponse +import net.gini.android.core.api.response.DocumentPageResponse import net.gini.android.core.api.response.PaymentRequestResponse import net.gini.android.core.api.response.PaymentResponse import okhttp3.RequestBody @@ -37,9 +37,22 @@ interface DocumentService { @DELETE suspend fun deleteDocumentFromUri(@HeaderMap bearer: Map, @Url documentUri: Uri): Response + @Deprecated( + "This function is deprecated. Use another one, please.", + replaceWith = ReplaceWith("getLayoutModel(documentId)")) @GET("documents/{documentId}/layout") suspend fun getLayoutForDocument(@HeaderMap bearer: Map, @Path("documentId") documentId: String): Response + @GET("documents/{documentId}/layout") + suspend fun getLayoutModel( + @HeaderMap bearer: Map, @Path("documentId") documentId: String + ) : Response + + @GET("documents/{documentId}/pages") + suspend fun getDocumentPages( + @HeaderMap bearer: Map, @Path("documentId") documentId: String + ) : Response> + @GET("paymentRequests/{id}") suspend fun getPaymentRequest(@HeaderMap bearer: Map, @Path("id") id: String): Response diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/mapper/DocumentLayoutMapper.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/mapper/DocumentLayoutMapper.kt new file mode 100644 index 0000000000..34b21b44ed --- /dev/null +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/mapper/DocumentLayoutMapper.kt @@ -0,0 +1,48 @@ +package net.gini.android.core.api.mapper + +import net.gini.android.core.api.models.DocumentLayout +import net.gini.android.core.api.response.DocumentLayoutResponse + + +fun DocumentLayoutResponse.toDocumentLayout() = DocumentLayout( + pages = pages.map { it.toPage() } +) + +fun DocumentLayoutResponse.PageResponse.toPage() = DocumentLayout.Page(number = number, + sizeX = sizeX, + sizeY = sizeY, + textZones = textZones.map { it.toTextZone() }, + regions = regions.map { it.toRegion() }) + +fun DocumentLayoutResponse.PageResponse.TextZoneResponse.toTextZone() = + DocumentLayout.Page.TextZone(paragraphs = paragraphs.map { it.toParagraph() }) + +fun DocumentLayoutResponse.PageResponse.TextZoneResponse.ParagraphResponse.toParagraph() = + DocumentLayout.Page.TextZone.Paragraph(width = width, + height = height, + top = top, + left = left, + lines = lines.map { it.toLine() }) + +fun DocumentLayoutResponse.PageResponse.TextZoneResponse.ParagraphResponse.LineResponse.toLine() = + DocumentLayout.Page.TextZone.Paragraph.Line(width = width, + height = height, + top = top, + left = left, + words = words.map { it.toWord() }) + +fun DocumentLayoutResponse.PageResponse.TextZoneResponse.ParagraphResponse.LineResponse.WordResponse.toWord() = + DocumentLayout.Page.TextZone.Paragraph.Line.Word( + width = width, + height = height, + top = top, + left = left, + fontSize = fontSize, + fontFamily = fontFamily, + bold = bold, + text = text + ) + +fun DocumentLayoutResponse.PageResponse.RegionResponse.toRegion() = DocumentLayout.Page.Region( + width = width, height = height, top = top, left = left, type = type +) \ No newline at end of file diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/mapper/DocumentPagesMapper.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/mapper/DocumentPagesMapper.kt new file mode 100644 index 0000000000..e5b3884f7c --- /dev/null +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/mapper/DocumentPagesMapper.kt @@ -0,0 +1,15 @@ +package net.gini.android.core.api.mapper + +import net.gini.android.core.api.models.DocumentPage +import net.gini.android.core.api.response.DocumentPageResponse + + +fun DocumentPageResponse.toDocumentPage() = DocumentPage( + pageNumber = pageNumber, + images = images.toImages() +) + +fun DocumentPageResponse.Images.toImages() = DocumentPage.Images( + medium = medium, + large = large +) \ No newline at end of file diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/models/DocumentLayout.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/models/DocumentLayout.kt new file mode 100644 index 0000000000..c365c5865c --- /dev/null +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/models/DocumentLayout.kt @@ -0,0 +1,57 @@ +package net.gini.android.core.api.models + +data class DocumentLayout( + val pages: List +) { + + data class Page( + val number: Int, + val sizeX: Float, + val sizeY: Float, + val textZones: List, + val regions: List + ) { + + data class TextZone( + val paragraphs: List, + ) { + + data class Paragraph( + val width: Float, + val height: Float, + val top: Float, + val left: Float, + val lines: List, + ) { + + data class Line( + val width: Float, + val height: Float, + val top: Float, + val left: Float, + val words: List, + ) { + + data class Word( + val width: Float, + val height: Float, + val top: Float, + val left: Float, + val fontSize: Float, + val fontFamily: String, + val bold: Boolean, + val text: String, + ) + } + } + } + + data class Region( + val width: Float, + val height: Float, + val top: Float, + val left: Float, + val type: String, + ) + } +} \ No newline at end of file diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/models/DocumentPage.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/models/DocumentPage.kt new file mode 100644 index 0000000000..be5c2586d8 --- /dev/null +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/models/DocumentPage.kt @@ -0,0 +1,12 @@ +package net.gini.android.core.api.models + + +data class DocumentPage( + val pageNumber: Int, + val images: Images +) { + data class Images( + val medium: String?, + val large: String?, + ) +} \ No newline at end of file diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/response/DocumentLayoutResponse.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/response/DocumentLayoutResponse.kt new file mode 100644 index 0000000000..bf0777d1c9 --- /dev/null +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/response/DocumentLayoutResponse.kt @@ -0,0 +1,67 @@ +package net.gini.android.core.api.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class DocumentLayoutResponse( + val pages: List +) { + + @JsonClass(generateAdapter = true) + data class PageResponse( + val number: Int, + val sizeX: Float, + val sizeY: Float, + val textZones: List, + val regions: List + ) { + + @JsonClass(generateAdapter = true) + data class TextZoneResponse( + val paragraphs: List, + ) { + + @JsonClass(generateAdapter = true) + data class ParagraphResponse( + @Json(name = "w") val width: Float, + @Json(name = "h") val height: Float, + @Json(name = "t") val top: Float, + @Json(name = "l") val left: Float, + val lines: List, + ) { + + @JsonClass(generateAdapter = true) + data class LineResponse( + @Json(name = "w") val width: Float, + @Json(name = "h") val height: Float, + @Json(name = "t") val top: Float, + @Json(name = "l") val left: Float, + @Json(name = "wds") val words: List, + ) { + + @JsonClass(generateAdapter = true) + data class WordResponse( + @Json(name = "w") val width: Float, + @Json(name = "h") val height: Float, + @Json(name = "t") val top: Float, + @Json(name = "l") val left: Float, + val fontSize: Float, + val fontFamily: String, + val bold: Boolean, + val text: String, + ) + } + } + } + + @JsonClass(generateAdapter = true) + data class RegionResponse( + @Json(name = "w") val width: Float, + @Json(name = "h") val height: Float, + @Json(name = "t") val top: Float, + @Json(name = "l") val left: Float, + val type: String, + ) + } +} \ No newline at end of file diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/response/DocumentPageResponse.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/response/DocumentPageResponse.kt new file mode 100644 index 0000000000..a0c841cff8 --- /dev/null +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/response/DocumentPageResponse.kt @@ -0,0 +1,16 @@ +package net.gini.android.core.api.response + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class DocumentPageResponse( + val pageNumber: Int, + val images: Images +) { + + @JsonClass(generateAdapter = true) + data class Images( + val medium: String?, + val large: String?, + ) +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c0e7bddd92..470f3edaca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -123,5 +123,6 @@ compose-tools-uiToolingPreview = { module = "androidx.compose.ui:ui-tooling-prev accompanist-themeAdapter = { module = "com.google.android.material:compose-theme-adapter-3", version.ref = "accompanist-themeAdapter" } koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" } koin-core = { module = "io.insert-koin:koin-core" } +koin-android-compat = { module = "io.insert-koin:koin-android-compat" } koin-android = { module = "io.insert-koin:koin-android" } koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose" } \ No newline at end of file diff --git a/health-api-library/library/src/test/java/net/gini/android/health/api/models/PageTest.kt b/health-api-library/library/src/test/java/net/gini/android/health/api/models/PageResponseTest.kt similarity index 99% rename from health-api-library/library/src/test/java/net/gini/android/health/api/models/PageTest.kt rename to health-api-library/library/src/test/java/net/gini/android/health/api/models/PageResponseTest.kt index 26f81612eb..15ef40fbc8 100644 --- a/health-api-library/library/src/test/java/net/gini/android/health/api/models/PageTest.kt +++ b/health-api-library/library/src/test/java/net/gini/android/health/api/models/PageResponseTest.kt @@ -15,7 +15,7 @@ import org.junit.runner.RunWith */ @RunWith(AndroidJUnit4::class) -class PageTest { +class PageResponseTest { @Test fun `finds largest image uri which is smaller than max size`() { From 860332328aa7f17f32c6299b727930fc4b104e20 Mon Sep 17 00:00:00 2001 From: Niko Date: Thu, 22 Aug 2024 15:01:54 +0200 Subject: [PATCH 2/4] feature(bank-sdk): Skonto Invoice Preview. Code refactor PP-643 --- .../capture/network/GiniCaptureDefaultNetworkService.kt | 2 +- .../main/java/net/gini/android/core/api/DocumentManager.kt | 4 ++-- .../java/net/gini/android/core/api/DocumentRemoteSource.kt | 4 ++-- .../main/java/net/gini/android/core/api/DocumentRepository.kt | 4 ++-- .../main/java/net/gini/android/core/api/DocumentService.kt | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/capture-sdk/default-network/src/main/java/net/gini/android/capture/network/GiniCaptureDefaultNetworkService.kt b/capture-sdk/default-network/src/main/java/net/gini/android/capture/network/GiniCaptureDefaultNetworkService.kt index 43a600e409..e764bab0a9 100644 --- a/capture-sdk/default-network/src/main/java/net/gini/android/capture/network/GiniCaptureDefaultNetworkService.kt +++ b/capture-sdk/default-network/src/main/java/net/gini/android/capture/network/GiniCaptureDefaultNetworkService.kt @@ -380,7 +380,7 @@ class GiniCaptureDefaultNetworkService( return launchCancellable { - when (val resource = giniBankApi.documentManager.getLayoutModel(documentId)) { + when (val resource = giniBankApi.documentManager.getDocumentLayout(documentId)) { is Resource.Cancelled -> { LOG.debug("Getting layout for document {} canceled", documentId) diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentManager.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentManager.kt index 676e34f3d1..5c38e52992 100644 --- a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentManager.kt +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentManager.kt @@ -172,8 +172,8 @@ abstract class DocumentManager, E: ExtractionsCont * @param documentId The document ID for which the layouts is requested. * @return [Resource] with a [DocumentLayout] instance containing the layout or information about the error */ - suspend fun getLayoutModel(documentId: String) : Resource = - documentRepository.getLayoutModel(documentId) + suspend fun getDocumentLayout(documentId: String) : Resource = + documentRepository.getDocumentLayout(documentId) /** * Get all extractions (specific and compound) for the given document. diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRemoteSource.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRemoteSource.kt index 745d2a13dd..4abfdb4e3f 100644 --- a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRemoteSource.kt +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRemoteSource.kt @@ -86,10 +86,10 @@ abstract class DocumentRemoteSource( response.body()?.string() ?: throw ApiException.forResponse("Empty response body", response) } - suspend fun getLayoutModel(accessToken: String, documentId: String): DocumentLayout = + suspend fun getDocumentLayout(accessToken: String, documentId: String): DocumentLayout = withContext(coroutineContext) { val response = SafeApiRequest.apiRequest { - documentService.getLayoutModel( + documentService.getDocumentLayout( bearerHeaderMap(accessToken, contentType = giniApiType.giniJsonMediaType), documentId ) } diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRepository.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRepository.kt index 03789e19c1..e7b2e14bbd 100644 --- a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRepository.kt +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentRepository.kt @@ -243,10 +243,10 @@ abstract class DocumentRepository( } } - suspend fun getLayoutModel(documentId: String) : Resource { + suspend fun getDocumentLayout(documentId: String) : Resource { return withAccessToken { accessToken -> wrapInResource { - documentRemoteSource.getLayoutModel(accessToken, documentId) + documentRemoteSource.getDocumentLayout(accessToken, documentId) } } } diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentService.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentService.kt index 24b9a1c66f..8cb3b36227 100644 --- a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentService.kt +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentService.kt @@ -44,7 +44,7 @@ interface DocumentService { suspend fun getLayoutForDocument(@HeaderMap bearer: Map, @Path("documentId") documentId: String): Response @GET("documents/{documentId}/layout") - suspend fun getLayoutModel( + suspend fun getDocumentLayout( @HeaderMap bearer: Map, @Path("documentId") documentId: String ) : Response From 79db3c769b49f4205e46e96632a18d54345d346d Mon Sep 17 00:00:00 2001 From: Niko Date: Thu, 22 Aug 2024 15:04:03 +0200 Subject: [PATCH 3/4] feature(bank-sdk): Skonto Invoice Preview. Code refactor PP-643 --- .../bank/sdk/capture/skonto/SkontoFragment.kt | 8 ++--- .../skonto/colors/SkontoScreenColors.kt | 30 ++++++++++++------- ...t => SkontoInvoicePreviewSectionColors.kt} | 6 ++-- 3 files changed, 26 insertions(+), 18 deletions(-) rename bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/colors/section/{SkontoInvoiceScanSectionColors.kt => SkontoInvoicePreviewSectionColors.kt} (92%) diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt index 88f0aee6d0..35516c0d43 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt @@ -78,7 +78,7 @@ import net.gini.android.bank.sdk.R import net.gini.android.bank.sdk.capture.skonto.colors.SkontoScreenColors import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoFooterSectionColors import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoInfoDialogColors -import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoInvoiceScanSectionColors +import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoInvoicePreviewSectionColors import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoSectionColors import net.gini.android.bank.sdk.capture.skonto.colors.section.WithoutSkontoSectionColors import net.gini.android.bank.sdk.capture.skonto.model.SkontoData @@ -323,7 +323,7 @@ private fun ScreenReadyState( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp), ) { - YourInvoiceScanSection( + InvoicePreviewSection( modifier = Modifier .padding(top = 8.dp) .tabletMaxWidth(), @@ -417,9 +417,9 @@ private fun NavigationActionBack( } @Composable -private fun YourInvoiceScanSection( +private fun InvoicePreviewSection( modifier: Modifier = Modifier, - colorScheme: SkontoInvoiceScanSectionColors, + colorScheme: SkontoInvoicePreviewSectionColors, onClick: () -> Unit, ) { Card( diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/colors/SkontoScreenColors.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/colors/SkontoScreenColors.kt index b0e44b5def..58b35ce4a1 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/colors/SkontoScreenColors.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/colors/SkontoScreenColors.kt @@ -5,7 +5,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoFooterSectionColors import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoInfoDialogColors -import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoInvoiceScanSectionColors +import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoInvoicePreviewSectionColors import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoSectionColors import net.gini.android.bank.sdk.capture.skonto.colors.section.WithoutSkontoSectionColors import net.gini.android.capture.ui.components.picker.date.GiniDatePickerDialogColors @@ -16,7 +16,7 @@ import net.gini.android.capture.ui.theme.GiniTheme data class SkontoScreenColors( val backgroundColor: Color, val topAppBarColors: GiniTopBarColors, - val invoiceScanSectionColors: SkontoInvoiceScanSectionColors, + val invoiceScanSectionColors: SkontoInvoicePreviewSectionColors, val skontoSectionColors: SkontoSectionColors, val withoutSkontoSectionColors: WithoutSkontoSectionColors, val footerSectionColors: SkontoFooterSectionColors, @@ -27,18 +27,26 @@ data class SkontoScreenColors( companion object { @Composable fun colors( - backgroundColor: Color = GiniTheme.colorScheme.background.primary, - topAppBarColors: GiniTopBarColors = GiniTopBarColors.colors(), - skontoInvoiceScanSectionColors: SkontoInvoiceScanSectionColors = SkontoInvoiceScanSectionColors.colors(), - discountSectionColors: SkontoSectionColors = SkontoSectionColors.colors(), - withoutSkontoSectionColors: WithoutSkontoSectionColors = WithoutSkontoSectionColors.colors(), - skontoFooterSectionColors: SkontoFooterSectionColors = SkontoFooterSectionColors.colors(), - datePickerColor: GiniDatePickerDialogColors = GiniDatePickerDialogColors.colors(), - infoDialogColors: SkontoInfoDialogColors = SkontoInfoDialogColors.colors(), + backgroundColor: Color = + GiniTheme.colorScheme.background.primary, + topAppBarColors: GiniTopBarColors = + GiniTopBarColors.colors(), + skontoInvoicePreviewSectionColors: SkontoInvoicePreviewSectionColors = + SkontoInvoicePreviewSectionColors.colors(), + discountSectionColors: SkontoSectionColors = + SkontoSectionColors.colors(), + withoutSkontoSectionColors: WithoutSkontoSectionColors = + WithoutSkontoSectionColors.colors(), + skontoFooterSectionColors: SkontoFooterSectionColors = + SkontoFooterSectionColors.colors(), + datePickerColor: GiniDatePickerDialogColors = + GiniDatePickerDialogColors.colors(), + infoDialogColors: SkontoInfoDialogColors = + SkontoInfoDialogColors.colors(), ) = SkontoScreenColors( backgroundColor = backgroundColor, topAppBarColors = topAppBarColors, - invoiceScanSectionColors = skontoInvoiceScanSectionColors, + invoiceScanSectionColors = skontoInvoicePreviewSectionColors, skontoSectionColors = discountSectionColors, withoutSkontoSectionColors = withoutSkontoSectionColors, footerSectionColors = skontoFooterSectionColors, diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/colors/section/SkontoInvoiceScanSectionColors.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/colors/section/SkontoInvoicePreviewSectionColors.kt similarity index 92% rename from bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/colors/section/SkontoInvoiceScanSectionColors.kt rename to bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/colors/section/SkontoInvoicePreviewSectionColors.kt index 5ac48d32fa..55c8a3aaf8 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/colors/section/SkontoInvoiceScanSectionColors.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/colors/section/SkontoInvoicePreviewSectionColors.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.graphics.Color import net.gini.android.capture.ui.theme.GiniTheme @Immutable -data class SkontoInvoiceScanSectionColors( +data class SkontoInvoicePreviewSectionColors( val cardBackgroundColor: Color, val titleTextColor: Color, val subtitleTextColor: Color, @@ -24,7 +24,7 @@ data class SkontoInvoiceScanSectionColors( iconBackgroundColor: Color = GiniTheme.colorScheme.placeholder.background, iconTint: Color = GiniTheme.colorScheme.placeholder.tint, arrowTint: Color = GiniTheme.colorScheme.icons.secondary, - ) = SkontoInvoiceScanSectionColors( + ) = SkontoInvoicePreviewSectionColors( cardBackgroundColor = cardBackgroundColor, titleTextColor = titleTextColor, subtitleTextColor = subtitleTextColor, @@ -33,4 +33,4 @@ data class SkontoInvoiceScanSectionColors( arrowTint = arrowTint, ) } -} \ No newline at end of file +} From 0287341f345dc6dddfe93f3ca209bd7add25d48e Mon Sep 17 00:00:00 2001 From: Niko Date: Thu, 22 Aug 2024 15:37:52 +0200 Subject: [PATCH 4/4] feature(bank-sdk): Skonto Invoice Preview. Code refactor (unit test fix) PP-643 --- .../analysis/AnalysisScreenPresenterTest.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/capture-sdk/sdk/src/test/java/net/gini/android/capture/analysis/AnalysisScreenPresenterTest.kt b/capture-sdk/sdk/src/test/java/net/gini/android/capture/analysis/AnalysisScreenPresenterTest.kt index 903aa8c27f..20805cfd05 100644 --- a/capture-sdk/sdk/src/test/java/net/gini/android/capture/analysis/AnalysisScreenPresenterTest.kt +++ b/capture-sdk/sdk/src/test/java/net/gini/android/capture/analysis/AnalysisScreenPresenterTest.kt @@ -394,7 +394,8 @@ class AnalysisScreenPresenterTest { val analysisFuture = CompletableFuture() analysisFuture.complete( AnalysisInteractor.ResultHolder( - AnalysisInteractor.Result.SUCCESS_NO_EXTRACTIONS + AnalysisInteractor.Result.SUCCESS_NO_EXTRACTIONS, + "dummy_doc_id", ) ) val presenter = @@ -431,7 +432,8 @@ class AnalysisScreenPresenterTest { val analysisFuture = CompletableFuture() analysisFuture.complete( AnalysisInteractor.ResultHolder( - AnalysisInteractor.Result.SUCCESS_NO_EXTRACTIONS + AnalysisInteractor.Result.SUCCESS_NO_EXTRACTIONS, + "dummy_doc_id" ) ) val presenter = @@ -465,7 +467,7 @@ class AnalysisScreenPresenterTest { analysisFuture.complete( AnalysisInteractor.ResultHolder( AnalysisInteractor.Result.SUCCESS_WITH_EXTRACTIONS, - extractions, compoundExtraction, returnReasons + extractions, compoundExtraction, returnReasons, "dummy_doc_id" ) ) val presenter = @@ -478,7 +480,7 @@ class AnalysisScreenPresenterTest { // Then verify(listener) - .onExtractionsAvailable(imageDocument, extractions, compoundExtraction, returnReasons) + .onExtractionsAvailable(extractions, compoundExtraction, returnReasons) } @Test @@ -490,7 +492,8 @@ class AnalysisScreenPresenterTest { val analysisFuture = CompletableFuture() analysisFuture.complete( AnalysisInteractor.ResultHolder( - AnalysisInteractor.Result.SUCCESS_NO_EXTRACTIONS + AnalysisInteractor.Result.SUCCESS_NO_EXTRACTIONS, + "dummy_doc_id" ) ) val presenter = spy( @@ -637,7 +640,8 @@ class AnalysisScreenPresenterTest { val analysisFuture = CompletableFuture() analysisFuture.complete( AnalysisInteractor.ResultHolder( - AnalysisInteractor.Result.SUCCESS_NO_EXTRACTIONS + AnalysisInteractor.Result.SUCCESS_NO_EXTRACTIONS, + "dummy_doc_id" ) ) val memoryStore = mock()