From 05a22c09d0e1b6df43be1878677195448052e511 Mon Sep 17 00:00:00 2001 From: Mahdi Abolfazli Date: Wed, 12 Jun 2024 11:42:34 +0200 Subject: [PATCH] feat(bank-sdk): Add BE integration to get configuration response PP-450 --- .../bank/api/BankApiDocumentManager.kt | 5 + .../bank/api/BankApiDocumentRemoteSource.kt | 9 + .../bank/api/BankApiDocumentRepository.kt | 7 + .../bank/api/BankApiDocumentService.kt | 4 + .../android/bank/api/models/Configuration.kt | 11 ++ .../api/response/ConfigurationResponse.kt | 25 +++ .../bank/sdk/capture/CaptureFlowActivity.kt | 26 +-- .../GiniCaptureDefaultNetworkService.kt | 104 +++++++++-- .../net/gini/android/capture/GiniCapture.java | 9 +- .../android/capture/GiniCaptureFragment.kt | 31 ++- .../capture/camera/CameraFragmentImpl.java | 2 - .../capture/internal/network/Configuration.kt | 14 ++ .../network/ConfigurationNetworkResult.kt | 8 + .../network/NetworkRequestsManager.java | 68 ++++++- .../network/GiniCaptureNetworkService.java | 6 +- .../UserAnalyticsEventTracker.kt | 176 +++++++++++++++++- 16 files changed, 456 insertions(+), 49 deletions(-) create mode 100644 bank-api-library/library/src/main/java/net/gini/android/bank/api/models/Configuration.kt create mode 100644 bank-api-library/library/src/main/java/net/gini/android/bank/api/response/ConfigurationResponse.kt create mode 100644 capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/Configuration.kt create mode 100644 capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/ConfigurationNetworkResult.kt diff --git a/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentManager.kt b/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentManager.kt index 251cc4cc90..217772acf7 100644 --- a/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentManager.kt +++ b/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentManager.kt @@ -1,5 +1,6 @@ package net.gini.android.bank.api; +import net.gini.android.bank.api.models.Configuration import net.gini.android.bank.api.models.ExtractionsContainer import net.gini.android.bank.api.models.ResolvePaymentInput import net.gini.android.bank.api.models.ResolvedPayment @@ -45,4 +46,8 @@ class BankApiDocumentManager internal constructor(private val documentRepository errorEvent: ErrorEvent ): Resource = documentRepository.logErrorEvent(errorEvent) + + + suspend fun getConfigurations(): Resource = + documentRepository.getConfigurations() } diff --git a/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentRemoteSource.kt b/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentRemoteSource.kt index 86c40bdfb1..9f158b73b4 100644 --- a/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentRemoteSource.kt +++ b/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentRemoteSource.kt @@ -1,11 +1,13 @@ package net.gini.android.bank.api import kotlinx.coroutines.withContext +import net.gini.android.bank.api.models.Configuration import net.gini.android.bank.api.models.ResolvePaymentInput import net.gini.android.bank.api.models.ResolvedPayment import net.gini.android.bank.api.models.toResolvedPayment import net.gini.android.bank.api.requests.ErrorEvent import net.gini.android.bank.api.requests.toResolvePaymentBody +import net.gini.android.bank.api.response.toConfiguration import net.gini.android.core.api.DocumentRemoteSource import net.gini.android.core.api.requests.ApiException import net.gini.android.core.api.requests.SafeApiRequest @@ -34,4 +36,11 @@ class BankApiDocumentRemoteSource internal constructor( documentService.logErrorEvent(bearerHeaderMap(accessToken, contentType = giniApiType.giniJsonMediaType), errorEvent) } } + + suspend fun getConfigurations(accessToken: String): Configuration = withContext(coroutineContext) { + val response = SafeApiRequest.apiRequest { + documentService.getConfigurations(bearerHeaderMap(accessToken, giniApiType.giniJsonMediaType)) + } + response.body()?.toConfiguration() ?: throw ApiException.forResponse("Empty response body", response) + } } diff --git a/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentRepository.kt b/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentRepository.kt index 00d18dd376..4d22f03e3d 100644 --- a/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentRepository.kt +++ b/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentRepository.kt @@ -45,6 +45,13 @@ class BankApiDocumentRepository( } } + suspend fun getConfigurations(): Resource = + withAccessToken { accessToken -> + wrapInResource { + documentRemoteSource.getConfigurations(accessToken) + } + } + @Throws(JSONException::class) private fun parseReturnReason(returnReasonsJson: JSONArray?): List { if (returnReasonsJson == null) { diff --git a/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentService.kt b/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentService.kt index 3fa1130b47..9a84e98527 100644 --- a/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentService.kt +++ b/bank-api-library/library/src/main/java/net/gini/android/bank/api/BankApiDocumentService.kt @@ -2,6 +2,7 @@ package net.gini.android.bank.api import net.gini.android.bank.api.requests.ErrorEvent import net.gini.android.bank.api.requests.ResolvePaymentBody +import net.gini.android.bank.api.response.ConfigurationResponse import net.gini.android.bank.api.response.ResolvePaymentResponse import net.gini.android.core.api.DocumentService import okhttp3.RequestBody @@ -19,4 +20,7 @@ internal interface BankApiDocumentService: DocumentService { @POST("events/error") suspend fun logErrorEvent(@HeaderMap bearer: Map, @Body errorEvent: ErrorEvent): Response + + @GET("configurations") + suspend fun getConfigurations(@HeaderMap bearer: Map): Response } diff --git a/bank-api-library/library/src/main/java/net/gini/android/bank/api/models/Configuration.kt b/bank-api-library/library/src/main/java/net/gini/android/bank/api/models/Configuration.kt new file mode 100644 index 0000000000..45331cccb6 --- /dev/null +++ b/bank-api-library/library/src/main/java/net/gini/android/bank/api/models/Configuration.kt @@ -0,0 +1,11 @@ +package net.gini.android.bank.api.models + +data class Configuration( + val clientID: String, + val isUserJourneyAnalyticsEnabled: Boolean, + val isSkontoEnabled: Boolean, + val isReturnAssistantEnabled: Boolean, + val mixpanelToken: String?, + val amplitudeApiKey: String?, + + ) \ No newline at end of file diff --git a/bank-api-library/library/src/main/java/net/gini/android/bank/api/response/ConfigurationResponse.kt b/bank-api-library/library/src/main/java/net/gini/android/bank/api/response/ConfigurationResponse.kt new file mode 100644 index 0000000000..a359832881 --- /dev/null +++ b/bank-api-library/library/src/main/java/net/gini/android/bank/api/response/ConfigurationResponse.kt @@ -0,0 +1,25 @@ +package net.gini.android.bank.api.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import net.gini.android.bank.api.models.Configuration + +@JsonClass(generateAdapter = true) +data class ConfigurationResponse( + @Json(name = "clientID") val clientID: String?, + @Json(name = "userJourneyAnalyticsEnabled") val userJourneyAnalyticsEnabled: Boolean?, + @Json(name = "skontoEnabled") val skontoEnabled: Boolean?, + @Json(name = "returnAssistantEnabled") val returnAssistantEnabled: Boolean?, + @Json(name = "mixpanelToken") val mixpanelToken: String?, + @Json(name = "amplitudeApiKey") val amplitudeApiKey: String?, +) + +internal fun ConfigurationResponse.toConfiguration() = Configuration( + clientID = clientID ?: "", + isUserJourneyAnalyticsEnabled = userJourneyAnalyticsEnabled ?: false, + isSkontoEnabled = skontoEnabled ?: false, + isReturnAssistantEnabled = returnAssistantEnabled ?: false, + mixpanelToken = mixpanelToken, + amplitudeApiKey = amplitudeApiKey +) + diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowActivity.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowActivity.kt index 400020fd3f..98bad0cc6f 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowActivity.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/CaptureFlowActivity.kt @@ -22,7 +22,8 @@ import net.gini.android.capture.tracking.useranalytics.properties.UserAnalyticsU */ internal class CaptureFlowActivity : AppCompatActivity(), CaptureFlowFragmentListener { - private val userAnalyticsEventTracker by lazy { UserAnalytics.getAnalyticsEventTracker() } + // TODO: move this to GiniCaptureFragment + //private val userAnalyticsEventTracker by lazy { UserAnalytics.getAnalyticsEventTracker() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -108,6 +109,7 @@ internal class CaptureFlowActivity : AppCompatActivity(), CaptureFlowFragmentLis const val EXTRA_OUT_RESULT = "GBS_EXTRA_OUT_RESULT" } + // TODO: uncomment the last part and move this to GiniCaptureFragment private fun setAnalyticsEntryPointProperty(isOpenWithDocumentExists: Boolean) { val entryPointProperty = if (isOpenWithDocumentExists) { @@ -120,17 +122,17 @@ internal class CaptureFlowActivity : AppCompatActivity(), CaptureFlowFragmentLis } ) } - - userAnalyticsEventTracker.setUserProperty( - setOf( - UserAnalyticsUserProperty.ReturnAssistantEnabled( - GiniBank.getCaptureConfiguration()?.returnAssistantEnabled ?: false - ), - UserAnalyticsUserProperty.ReturnReasonsEnabled(GiniBank.enableReturnReasons), - ) - ) - - userAnalyticsEventTracker.setEventSuperProperty(entryPointProperty) +// +// userAnalyticsEventTracker.setUserProperty( +// setOf( +// UserAnalyticsUserProperty.ReturnAssistantEnabled( +// GiniBank.getCaptureConfiguration()?.returnAssistantEnabled ?: false +// ), +// UserAnalyticsUserProperty.ReturnReasonsEnabled(GiniBank.enableReturnReasons), +// ) +// ) +// +// userAnalyticsEventTracker.setEventSuperProperty(entryPointProperty) } } 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 fb43667cc4..33d81f0586 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 @@ -12,6 +12,8 @@ import net.gini.android.bank.api.GiniBankAPIBuilder import net.gini.android.capture.Document import net.gini.android.capture.GiniCapture import net.gini.android.capture.document.GiniCaptureMultiPageDocument +import net.gini.android.capture.internal.network.Configuration +import net.gini.android.bank.api.models.Configuration as BankConfiguration import net.gini.android.capture.logging.ErrorLog import net.gini.android.capture.network.GiniCaptureDefaultNetworkService.Companion.builder import net.gini.android.capture.network.logging.formattedErrorMessage @@ -25,6 +27,7 @@ import net.gini.android.core.api.authorization.SessionManager import okhttp3.Cache import org.slf4j.Logger import org.slf4j.LoggerFactory +import java.util.UUID import java.util.concurrent.TimeUnit import javax.net.ssl.TrustManager import kotlin.coroutines.CoroutineContext @@ -59,7 +62,8 @@ class GiniCaptureDefaultNetworkService( ) : GiniCaptureNetworkService { private val coroutineScope = CoroutineScope(coroutineContext) - private val giniApiDocuments: MutableMap = mutableMapOf() + private val giniApiDocuments: MutableMap = + mutableMapOf() /** * Contains the document which was created when the user uploaded an image or a pdf for @@ -76,6 +80,46 @@ class GiniCaptureDefaultNetworkService( var analyzedGiniApiDocument: net.gini.android.core.api.models.Document? = null private set + override fun getConfiguration(callback: GiniCaptureNetworkCallback): CancellationToken? = + launchCancellable { + when (val configurationResource = giniBankApi.documentManager.getConfigurations()) { + is Resource.Success -> { + LOG.debug( + "Get configuration success" + ) + callback.success(mapBankConfigurationToConfiguration(configurationResource.data)) + } + + is Resource.Error -> { + LOG.debug( + "Get configuration error for {}: {}" + ) + val error = Error(configurationResource.formattedErrorMessage) + LOG.error( + "Document deletion failed for api id {}", + error.message + ) + callback.failure(error) + } + + is Resource.Cancelled -> { + LOG.debug("Get configuration cancelled for") + callback.cancelled() + } + } + } + + private fun mapBankConfigurationToConfiguration(configuration: BankConfiguration) = + Configuration( + UUID.randomUUID(), + configuration.clientID, + configuration.isUserJourneyAnalyticsEnabled, + configuration.isSkontoEnabled, + configuration.isReturnAssistantEnabled, + configuration.mixpanelToken ?: "", + configuration.amplitudeApiKey ?: "", + ) + override fun upload( document: Document, callback: GiniCaptureNetworkCallback @@ -119,16 +163,20 @@ class GiniCaptureDefaultNetworkService( giniApiDocuments[apiDocument.id] = apiDocument callback.success(Result(apiDocument.id)) } + is Resource.Error -> { - val error = Error(partialDocumentResource.responseStatusCode, - partialDocumentResource.responseHeaders, partialDocumentResource.exception) + val error = Error( + partialDocumentResource.responseStatusCode, + partialDocumentResource.responseHeaders, partialDocumentResource.exception + ) LOG.error( "Document upload failed for {}: {}", document.id, error.message ) callback.failure(error) } + is Resource.Cancelled -> { LOG.debug( "Document upload cancelled for {}", @@ -151,12 +199,14 @@ class GiniCaptureDefaultNetworkService( callback: GiniCaptureNetworkCallback ): CancellationToken = launchCancellable { LOG.debug("Delete document with api id {}", giniApiDocumentId) - val deleteResource = giniBankApi.documentManager.deletePartialDocumentAndParents(giniApiDocumentId) + val deleteResource = + giniBankApi.documentManager.deletePartialDocumentAndParents(giniApiDocumentId) when (deleteResource) { is Resource.Success -> { LOG.debug("Document deletion success for api id {}", giniApiDocumentId) callback.success(Result(giniApiDocumentId)) } + is Resource.Error -> { val error = Error(deleteResource.formattedErrorMessage) LOG.error( @@ -165,6 +215,7 @@ class GiniCaptureDefaultNetworkService( ) callback.failure(error) } + is Resource.Cancelled -> { LOG.debug("Document deletion cancelled for api id {}", giniApiDocumentId) callback.cancelled() @@ -199,9 +250,10 @@ class GiniCaptureDefaultNetworkService( .mapSuccess { compositeDocumentResource -> val compositeDocument = compositeDocumentResource.data giniApiDocuments[compositeDocument.id] = compositeDocument - giniBankApi.documentManager.getAllExtractionsWithPolling(compositeDocument).mapSuccess { - Resource.Success(compositeDocument to it.data) - } + giniBankApi.documentManager.getAllExtractionsWithPolling(compositeDocument) + .mapSuccess { + Resource.Success(compositeDocument to it.data) + } } when (compositeDocumentAndExtractionsResource) { is Resource.Cancelled -> { @@ -210,6 +262,7 @@ class GiniCaptureDefaultNetworkService( giniApiDocumentIdRotationMap ) } + is Resource.Error -> { val error = Error(compositeDocumentAndExtractionsResource.formattedErrorMessage) LOG.error( @@ -218,22 +271,33 @@ class GiniCaptureDefaultNetworkService( ) callback.failure(error) } + is Resource.Success -> { val compositeDocument = compositeDocumentAndExtractionsResource.data.first val allExtractions = compositeDocumentAndExtractionsResource.data.second analyzedGiniApiDocument = compositeDocument - val extractions = SpecificExtractionMapper.mapToGiniCapture(allExtractions.specificExtractions) - val compoundExtractions = CompoundExtractionsMapper.mapToGiniCapture(allExtractions.compoundExtractions) - val returnReasons = ReturnReasonsMapper.mapToGiniCapture(allExtractions.returnReasons) + val extractions = + SpecificExtractionMapper.mapToGiniCapture(allExtractions.specificExtractions) + val compoundExtractions = + CompoundExtractionsMapper.mapToGiniCapture(allExtractions.compoundExtractions) + val returnReasons = + ReturnReasonsMapper.mapToGiniCapture(allExtractions.returnReasons) LOG.debug( "Document analysis success for documents {}: extractions = {}; compoundExtractions = {}; returnReasons = {}", giniApiDocumentIdRotationMap, extractions, compoundExtractions, returnReasons ) - callback.success(AnalysisResult(compositeDocument.id, extractions, compoundExtractions, returnReasons)) + callback.success( + AnalysisResult( + compositeDocument.id, + extractions, + compoundExtractions, + returnReasons + ) + ) } } } @@ -249,7 +313,10 @@ class GiniCaptureDefaultNetworkService( // We require the Gini Bank API lib's net.gini.android.core.api.models.Document for sending the feedback if (document != null) { val feedbackResource = if (compoundExtractions.isEmpty()) { - documentManager.sendFeedbackForExtractions(document, SpecificExtractionMapper.mapToApiSdk(extractions)) + documentManager.sendFeedbackForExtractions( + document, + SpecificExtractionMapper.mapToApiSdk(extractions) + ) } else { documentManager.sendFeedbackForExtractions( document, @@ -265,9 +332,14 @@ class GiniCaptureDefaultNetworkService( ) callback.success(null) } + is Resource.Error -> { val error = Error(feedbackResource.formattedErrorMessage) - LOG.error("Send feedback failed for api document {}: {}", document.id, error.message) + LOG.error( + "Send feedback failed for api document {}: {}", + document.id, + error.message + ) handleErrorLog( ErrorLog( description = "Failed to send feedback for document ${document.id}", @@ -276,8 +348,9 @@ class GiniCaptureDefaultNetworkService( ) callback.failure(error) } + is Resource.Cancelled -> { - LOG.debug( + LOG.debug( "Send feedback cancelled for api document {}", document.id ) @@ -547,7 +620,8 @@ class GiniCaptureDefaultNetworkService( } companion object { - private val LOG: Logger = LoggerFactory.getLogger(GiniCaptureDefaultNetworkService::class.java) + private val LOG: Logger = + LoggerFactory.getLogger(GiniCaptureDefaultNetworkService::class.java) /** * Creates a new [GiniCaptureDefaultNetworkService.Builder] to configure and create a new diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/GiniCapture.java b/capture-sdk/sdk/src/main/java/net/gini/android/capture/GiniCapture.java index b0da3eb314..3579902d81 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/GiniCapture.java +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/GiniCapture.java @@ -1,5 +1,9 @@ package net.gini.android.capture; +import static net.gini.android.capture.internal.util.FileImportValidator.FILE_SIZE_LIMIT; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; + import android.content.Context; import android.content.Intent; @@ -56,10 +60,6 @@ import java.util.List; import java.util.Map; -import static java.util.Collections.emptyList; -import static java.util.Collections.emptyMap; -import static net.gini.android.capture.internal.util.FileImportValidator.FILE_SIZE_LIMIT; - /** * Created by Alpar Szotyori on 22.02.2018. *

@@ -186,7 +186,6 @@ public static synchronized Builder newInstance(final Context context) { } cleanup(context); } - UserAnalytics.INSTANCE.initialize(context); return new Builder(); } 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 fbf2a19fef..96d072653c 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 @@ -25,14 +25,16 @@ import net.gini.android.capture.network.model.GiniCaptureReturnReason import net.gini.android.capture.network.model.GiniCaptureSpecificExtraction import net.gini.android.capture.noresults.NoResultsFragment import net.gini.android.capture.review.multipage.MultiPageReviewFragment +import net.gini.android.capture.tracking.useranalytics.UserAnalytics +import java.util.UUID + class GiniCaptureFragment(private val openWithDocument: Document? = null) : Fragment(), CameraFragmentListener, AnalysisFragmentListener, EnterManuallyButtonListener, - CancelListener -{ + CancelListener { private lateinit var navController: NavController private lateinit var giniCaptureFragmentListener: GiniCaptureFragmentListener @@ -53,11 +55,27 @@ class GiniCaptureFragment(private val openWithDocument: Document? = null) : cameraListener = this, analysisFragmentListener = this, enterManuallyButtonListener = this, - cancelListener = this) + cancelListener = this + ) super.onCreate(savedInstanceState) if (GiniCapture.hasInstance() && !GiniCapture.getInstance().allowScreenshots) { requireActivity().window.disallowScreenshots() } + + if (GiniCapture.hasInstance()) { + UserAnalytics.initialize(requireActivity()) + val networkRequestsManager = + GiniCapture.getInstance().internal().networkRequestsManager + val response = networkRequestsManager + ?.getConfigurations(UUID.randomUUID()) + response?.thenAcceptAsync { res -> + UserAnalytics.setPlatformTokens( + mixpanelToken = res.configuration.mixpanelToken, + amplitudeApiKey = res.configuration.amplitudeApiKey + ) + } + + } } override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater { @@ -87,7 +105,10 @@ class GiniCaptureFragment(private val openWithDocument: Document? = null) : ) ) } else { - if (shouldShowOnboarding() || (shouldShowOnboardingAtFirstRun() && !oncePerInstallEventStore.containsEvent(OncePerInstallEvent.SHOW_ONBOARDING))) { + if (shouldShowOnboarding() || (shouldShowOnboardingAtFirstRun() && !oncePerInstallEventStore.containsEvent( + OncePerInstallEvent.SHOW_ONBOARDING + )) + ) { oncePerInstallEventStore.saveEvent(OncePerInstallEvent.SHOW_ONBOARDING) navController.navigate(CameraFragmentDirections.toOnboardingFragment()) } @@ -222,7 +243,7 @@ class CaptureFragmentFactory( analysisFragmentListener ) setCancelListener(cancelListener) - } + } ErrorFragment::class.java.name -> return ErrorFragment().apply { setListener( diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/camera/CameraFragmentImpl.java b/capture-sdk/sdk/src/main/java/net/gini/android/capture/camera/CameraFragmentImpl.java index 1af02479ef..0409148015 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/camera/CameraFragmentImpl.java +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/camera/CameraFragmentImpl.java @@ -343,8 +343,6 @@ public void onCreate(final Bundle savedInstanceState) { if (savedInstanceState != null) { restoreSavedState(savedInstanceState); } - // TODO Remove it after migration to GiniCapture.initialize(context) - next major release (from version 3 to 4) - UserAnalytics.INSTANCE.initialize(this.mFragment.getActivity().getApplicationContext()); } diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/Configuration.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/Configuration.kt new file mode 100644 index 0000000000..5edb11e7fd --- /dev/null +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/Configuration.kt @@ -0,0 +1,14 @@ +package net.gini.android.capture.internal.network + +import java.util.UUID + +data class Configuration( + val id: UUID = UUID.randomUUID(), + val clientID: String, + val isUserJourneyAnalyticsEnabled: Boolean, + val isSkontoEnabled: Boolean, + val isReturnAssistantEnabled: Boolean, + val mixpanelToken: String, + val amplitudeApiKey: String, + + ) diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/ConfigurationNetworkResult.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/ConfigurationNetworkResult.kt new file mode 100644 index 0000000000..206eb5200e --- /dev/null +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/ConfigurationNetworkResult.kt @@ -0,0 +1,8 @@ +package net.gini.android.capture.internal.network + +import java.util.UUID + +data class ConfigurationNetworkResult( + val configuration: Configuration, + val id: UUID +) \ No newline at end of file diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/NetworkRequestsManager.java b/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/NetworkRequestsManager.java index 6794644559..a93a5fa8d8 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/NetworkRequestsManager.java +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/internal/network/NetworkRequestsManager.java @@ -2,7 +2,7 @@ /** * Created by Alpar Szotyori on 13.04.2018. - * + *

* Copyright (c) 2018 Gini GmbH. */ @@ -35,6 +35,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.CancellationException; import jersey.repackaged.jsr166e.CompletableFuture; @@ -55,20 +56,83 @@ public class NetworkRequestsManager { mDocumentDeleteFutures; private final Map>> mDocumentAnalyzeFutures; + private final Map> mConfigurationFutures; private final GiniCaptureNetworkService mGiniCaptureNetworkService; private final DocumentDataMemoryCache mDocumentDataMemoryCache; public NetworkRequestsManager(@NonNull final GiniCaptureNetworkService giniCaptureNetworkService, - @NonNull final DocumentDataMemoryCache documentDataMemoryCache) { + @NonNull final DocumentDataMemoryCache documentDataMemoryCache) { mGiniCaptureNetworkService = giniCaptureNetworkService; mDocumentDataMemoryCache = documentDataMemoryCache; mApiDocumentIds = new HashMap<>(); mDocumentUploadFutures = new HashMap<>(); mDocumentDeleteFutures = new HashMap<>(); mDocumentAnalyzeFutures = new HashMap<>(); + mConfigurationFutures = new HashMap<>(); } + public CompletableFuture getConfigurations(UUID id) { + + final CompletableFuture configurationFuture = + mConfigurationFutures.get(id); + if (configurationFuture != null) { + return configurationFuture; + } + + final CompletableFuture future = + new CompletableFuture<>(); + mConfigurationFutures.put(id, future); + + final CancellationToken cancellationToken = + mGiniCaptureNetworkService.getConfiguration( + new GiniCaptureNetworkCallback() { + @Override + public void failure(final Error error) { + LOG.error("Get configuration failed for {}: {}", + id, + error.getMessage()); + ErrorType errorType = ErrorType.typeFromError(error); + future.completeExceptionally(new FailureException(errorType)); + } + + @Override + public void success(final Configuration result) { + LOG.debug("Get configuration success for {}: {}", + id, + result); + future.complete(new ConfigurationNetworkResult(result, + result.getId())); + } + + @Override + public void cancelled() { + LOG.debug("Get configuration cancelled for {}", + id); + future.cancel(false); + + } + }); + + future.handle( + (requestResult, throwable) -> { + if (throwable != null) { + if (isCancellation(throwable)) { + cancellationToken.cancel(); + } else { + ErrorLogger.log(new ErrorLog("Getting configuration failed", throwable)); + } + } else if (requestResult != null) { + mConfigurationFutures.remove(id); + } + mConfigurationFutures.remove(id); + return requestResult; + }); + + return future; + } + + public CompletableFuture> upload( @NonNull final Context context, @NonNull final GiniCaptureDocument document) { 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 57a0423a5f..a4982ff97e 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 @@ -6,7 +6,7 @@ import net.gini.android.capture.Document; import net.gini.android.capture.GiniCapture; -import net.gini.android.capture.analysis.AnalysisFragmentListener; +import net.gini.android.capture.internal.network.Configuration; import net.gini.android.capture.logging.ErrorLog; import net.gini.android.capture.logging.ErrorLoggerListener; import net.gini.android.capture.network.model.GiniCaptureCompoundExtraction; @@ -110,6 +110,10 @@ void sendFeedback(@NonNull final Map extr */ void cleanup(); + default CancellationToken getConfiguration(@NonNull final GiniCaptureNetworkCallback callback) { + return null; + } + @Override default void handleErrorLog(@NonNull ErrorLog errorLog) { } diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/UserAnalyticsEventTracker.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/UserAnalyticsEventTracker.kt index bd903236bc..065e722c62 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/UserAnalyticsEventTracker.kt +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/tracking/useranalytics/UserAnalyticsEventTracker.kt @@ -1,12 +1,16 @@ package net.gini.android.capture.tracking.useranalytics import android.content.Context +import android.util.Log import com.mixpanel.android.mpmetrics.MixpanelAPI -import net.gini.android.capture.R import net.gini.android.capture.internal.provider.InstallationIdProvider +import net.gini.android.capture.tracking.useranalytics.properties.AnalyticsKeyPairProperty import net.gini.android.capture.tracking.useranalytics.properties.UserAnalyticsEventProperty import net.gini.android.capture.tracking.useranalytics.properties.UserAnalyticsEventSuperProperty import net.gini.android.capture.tracking.useranalytics.properties.UserAnalyticsUserProperty +import org.slf4j.LoggerFactory +import java.util.LinkedList +import java.util.Queue interface UserAnalyticsEventTracker { @@ -26,11 +30,20 @@ object UserAnalytics { private var eventTracker: UserAnalyticsEventTracker? = null - fun initialize(applicationContext: Context) { + fun initialize( + applicationContext: Context + ) { if (eventTracker != null) return eventTracker = - createAnalyticsEventTracker(EventTrackerPlatform.MIXPANEL, applicationContext) + BufferedUserAnalyticsEventTracker(applicationContext) + } + + fun setPlatformTokens(mixpanelToken: String, amplitudeApiKey: String) { + (eventTracker as? BufferedUserAnalyticsEventTracker)?.setPlatformTokens( + mixpanelToken, + amplitudeApiKey + ) } fun getAnalyticsEventTracker( @@ -42,19 +55,165 @@ object UserAnalytics { eventTracker = null } + +} + +private class BufferedUserAnalyticsEventTracker(val context: Context) : UserAnalyticsEventTracker { + + private val LOG = LoggerFactory.getLogger(BufferedUserAnalyticsEventTracker::class.java) + private var eventTracker: UserAnalyticsEventTracker? = null + private val eventSuperProperties: Queue> = LinkedList() + private val userProperties: Queue> = LinkedList() + private val events: Queue>> = + LinkedList() + + fun setPlatformTokens(mixpanelToken: String, amplitudeApiKey: String) { + val platform: EventTrackerPlatform? = + if (mixpanelToken.isNotEmpty() && amplitudeApiKey.isNotEmpty()) { + EventTrackerPlatform.BOTH + } else if (mixpanelToken.isNotEmpty()) { + EventTrackerPlatform.MIXPANEL + } else if (amplitudeApiKey.isNotEmpty()) { + EventTrackerPlatform.AMPLITUDE + } else { + LOG.debug("No platform token provided") + null + } + if (platform != null) { + eventTracker = createAnalyticsEventTracker( + platform, + context, + mixpanelToken, + amplitudeApiKey + ) + trySendEvents() + } + + } + + + override fun setEventSuperProperty(property: Set) { + this.eventSuperProperties.add(property) + trySendEvents() + } + + override fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty) { + setEventSuperProperty(setOf(property)) + } + + override fun setUserProperty(userProperties: Set) { + this.userProperties.add(userProperties) + trySendEvents() + } + + override fun setUserProperty(userProperty: UserAnalyticsUserProperty) { + setUserProperty(setOf(userProperty)) + } + + override fun trackEvent( + eventName: UserAnalyticsEvent, + properties: Set + ) { + events.add(Pair(eventName, properties)) + trySendEvents() + } + + override fun trackEvent(eventName: UserAnalyticsEvent) { + trackEvent(eventName, emptySet()) + } + + private fun trySendEvents() { + if (eventTracker == null) { + return + } else { + while (eventSuperProperties.isNotEmpty()) { + eventSuperProperties.poll()?.forEach { + eventTracker?.setEventSuperProperty(it as UserAnalyticsEventSuperProperty) + } + } + while (userProperties.isNotEmpty()) { + userProperties.poll()?.forEach { + eventTracker?.setUserProperty(it as UserAnalyticsUserProperty) + } + } + while (events.isNotEmpty()) { + val event = events.poll() + if (event != null) + eventTracker?.trackEvent(event.first, event.second) + } + } + } + private fun createAnalyticsEventTracker( platform: EventTrackerPlatform, - applicationContext: Context + applicationContext: Context, + mixpanelToken: String, + amplitudeApiKey: String ): UserAnalyticsEventTracker { return when (platform) { - EventTrackerPlatform.MIXPANEL -> MixPanelUserAnalyticsEventTracker(applicationContext) + EventTrackerPlatform.MIXPANEL -> MixPanelUserAnalyticsEventTracker( + applicationContext, + mixpanelToken + ) + + EventTrackerPlatform.AMPLITUDE -> TODO() + EventTrackerPlatform.BOTH -> MultiUserAnalyticsEventTracker( + applicationContext, + mixpanelToken, + amplitudeApiKey + ) + + else -> throw IllegalStateException("No platform token provided") } } + +} + +private class MultiUserAnalyticsEventTracker( + context: Context, + mixpanelToken: String, + amplitudeApiKey: String +) : UserAnalyticsEventTracker { + + private val mixpanelEventTracker: UserAnalyticsEventTracker + + init { + mixpanelEventTracker = + MixPanelUserAnalyticsEventTracker(context, mixpanelToken) + } + + override fun setUserProperty(userProperties: Set) { + mixpanelEventTracker.setUserProperty(userProperties) + } + + override fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty) { + setEventSuperProperty(setOf(property)) + } + + override fun setEventSuperProperty(property: Set) { + mixpanelEventTracker.setEventSuperProperty(property) + } + + override fun setUserProperty(userProperty: UserAnalyticsUserProperty) { + setUserProperty(setOf(userProperty)) + } + + override fun trackEvent(eventName: UserAnalyticsEvent) { + trackEvent(eventName, emptySet()) + } + + override fun trackEvent( + eventName: UserAnalyticsEvent, + properties: Set + ) { + mixpanelEventTracker.trackEvent(eventName, properties) + } } private class MixPanelUserAnalyticsEventTracker( context: Context, + mixpanelToken: String, installationIdProvider: InstallationIdProvider = InstallationIdProvider(context) ) : UserAnalyticsEventTracker { @@ -62,7 +221,7 @@ private class MixPanelUserAnalyticsEventTracker( init { mixpanelAPI = - MixpanelAPI.getInstance(context, context.getString(R.string.mixpanel_api_key), false) + MixpanelAPI.getInstance(context, mixpanelToken, false) mixpanelAPI.identify(installationIdProvider.getInstallationId()) } @@ -96,5 +255,8 @@ private class MixPanelUserAnalyticsEventTracker( } enum class EventTrackerPlatform { - MIXPANEL + MIXPANEL, + AMPLITUDE, + BOTH, + NONE }