diff --git a/bank-sdk/sdk/src/androidTest/java/net/gini/android/bank/sdk/TransferSummaryIntegrationTest.kt b/bank-sdk/sdk/src/androidTest/java/net/gini/android/bank/sdk/TransferSummaryIntegrationTest.kt index e380222d7f..6f8bb74e89 100644 --- a/bank-sdk/sdk/src/androidTest/java/net/gini/android/bank/sdk/TransferSummaryIntegrationTest.kt +++ b/bank-sdk/sdk/src/androidTest/java/net/gini/android/bank/sdk/TransferSummaryIntegrationTest.kt @@ -267,5 +267,7 @@ class TransferSummaryIntegrationTest { override fun getSource(): Document.Source = Document.Source.newSource("androidTest") override fun isReviewable(): Boolean = false + + override fun generateUploadMetadata(context: Context?): String = "" } } \ No newline at end of file diff --git a/capture-sdk/default-network/src/androidTest/java/net/gini/android/capture/network/TransferSummaryIntegrationTest.kt b/capture-sdk/default-network/src/androidTest/java/net/gini/android/capture/network/TransferSummaryIntegrationTest.kt index e4da8da0d9..6b102e068e 100644 --- a/capture-sdk/default-network/src/androidTest/java/net/gini/android/capture/network/TransferSummaryIntegrationTest.kt +++ b/capture-sdk/default-network/src/androidTest/java/net/gini/android/capture/network/TransferSummaryIntegrationTest.kt @@ -262,5 +262,7 @@ class TransferSummaryIntegrationTest { override fun getSource(): Document.Source = Document.Source.newSource("androidTest") override fun isReviewable(): Boolean = false + + override fun generateUploadMetadata(context: Context?): String = "" } } \ No newline at end of file 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 b0493c9e6b..e33ad54592 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 @@ -2,6 +2,7 @@ package net.gini.android.capture.network import android.content.Context import android.text.TextUtils +import androidx.annotation.VisibleForTesting import androidx.annotation.XmlRes import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -57,10 +58,14 @@ import net.gini.android.bank.api.models.Configuration as BankConfiguration * [GiniCapture.Builder.setGiniCaptureNetworkService] when creating a * [GiniCapture] instance. */ -class GiniCaptureDefaultNetworkService( +class GiniCaptureDefaultNetworkService + +@VisibleForTesting +internal constructor( internal val giniBankApi: GiniBankAPI, private val documentMetadata: DocumentMetadata?, - coroutineContext: CoroutineContext = Dispatchers.Main + private val context: Context, + coroutineContext: CoroutineContext = Dispatchers.Main, ) : GiniCaptureNetworkService { private val coroutineScope = CoroutineScope(coroutineContext) @@ -212,12 +217,18 @@ class GiniCaptureDefaultNetworkService( return@launchCancellable } + val uploadMetadata = document.generateUploadMetadata(context) + val partialDocumentResource = giniBankApi.documentManager.createPartialDocument( document = documentData, contentType = document.mimeType, filename = null, documentType = null, - documentMetadata + documentMetadata?.copy()?.apply { + setUploadMetadata(uploadMetadata) + } ?: DocumentMetadata().apply { + setUploadMetadata(uploadMetadata) + } ) when (partialDocumentResource) { @@ -512,7 +523,7 @@ class GiniCaptureDefaultNetworkService( trustManager?.let { giniApiBuilder.setTrustManager(it) } giniApiBuilder.setDebuggingEnabled(isDebuggingEnabled) val giniBankApi = giniApiBuilder.build() - return GiniCaptureDefaultNetworkService(giniBankApi, documentMetadata) + return GiniCaptureDefaultNetworkService(giniBankApi, documentMetadata, mContext) } /** diff --git a/capture-sdk/default-network/src/test/java/net/gini/android/capture/network/GiniCaptureDefaultNetworkServiceTest.kt b/capture-sdk/default-network/src/test/java/net/gini/android/capture/network/GiniCaptureDefaultNetworkServiceTest.kt index f9766942c8..16aa856d37 100644 --- a/capture-sdk/default-network/src/test/java/net/gini/android/capture/network/GiniCaptureDefaultNetworkServiceTest.kt +++ b/capture-sdk/default-network/src/test/java/net/gini/android/capture/network/GiniCaptureDefaultNetworkServiceTest.kt @@ -2,6 +2,7 @@ package net.gini.android.capture.network import android.net.Uri import android.os.Looper +import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import io.mockk.* @@ -13,7 +14,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.Shadows.shadowOf import java.util.* -import kotlin.collections.LinkedHashMap /** * Created by Alpár Szotyori on 25.02.22. @@ -56,7 +56,7 @@ class GiniCaptureDefaultNetworkServiceTest { // Mock DocumentTaskManager returning the mock documents val documentManager = mockk() - coEvery { documentManager.createPartialDocument(any(), any(), null, null) } returns Resource.Success(partialDocument) + coEvery { documentManager.createPartialDocument(any(), any(), null, null, any()) } returns Resource.Success(partialDocument) coEvery { documentManager.createCompositeDocument(any>(), any()) } returns Resource.Success(compositeDocument) coEvery { documentManager.pollDocument(any()) } returns Resource.Success(compositeDocument) coEvery { documentManager.getAllExtractionsWithPolling(any()) } returns Resource.Success(mockk()) @@ -65,13 +65,14 @@ class GiniCaptureDefaultNetworkServiceTest { val bankApi = mockk() every { bankApi.documentManager } returns documentManager - val networkService = GiniCaptureDefaultNetworkService(bankApi, null) + val networkService = GiniCaptureDefaultNetworkService(bankApi, null, ApplicationProvider.getApplicationContext()) // Mock Gini Capture SDK document val captureDocument = mockk() every { captureDocument.id } returns "id" every { captureDocument.data } returns byteArrayOf() every { captureDocument.mimeType } returns "image/jpeg" + every { captureDocument.generateUploadMetadata(ApplicationProvider.getApplicationContext()) } returns "" // When networkService.upload(captureDocument, mockk(relaxed = true)) diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/Document.java b/capture-sdk/sdk/src/main/java/net/gini/android/capture/Document.java index 1aed8c6c6c..d09b594815 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/Document.java +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/Document.java @@ -1,16 +1,17 @@ package net.gini.android.capture; +import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; -import java.util.HashMap; -import java.util.Map; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; + /** * This class is the container for transferring documents between the client application and the @@ -120,6 +121,12 @@ public interface Document extends Parcelable { */ boolean isReviewable(); + /** + * Generate metadata to be sent to backend when creating partial document. + * + */ + String generateUploadMetadata(Context context); + /** * Supported document types. */ diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/document/GiniCaptureDocument.java b/capture-sdk/sdk/src/main/java/net/gini/android/capture/document/GiniCaptureDocument.java index 63591f4a11..4e9b85c574 100644 --- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/document/GiniCaptureDocument.java +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/document/GiniCaptureDocument.java @@ -5,18 +5,19 @@ import android.net.Uri; import android.os.Parcel; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import net.gini.android.capture.AsyncCallback; import net.gini.android.capture.Document; import net.gini.android.capture.internal.camera.photo.ParcelableMemoryCache; +import net.gini.android.capture.internal.util.DeviceHelper; import net.gini.android.capture.internal.util.UriReaderAsyncTask; import net.gini.android.capture.util.IntentHelper; import java.util.Arrays; import java.util.UUID; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - /** * Internal use only. * @@ -219,6 +220,16 @@ public String getParcelableMemoryCacheTag() { return mParcelableMemoryCacheTag; } + @Override + public String generateUploadMetadata(Context context) { + return UploadMetadata.INSTANCE + .setSource(mSource.getName()) + .setDeviceType(DeviceHelper.getDeviceType(context)) + .setDeviceOrientation(DeviceHelper.getDeviceOrientation(context)) + .setImportMethod(mImportMethod.name()) + .build(); + } + @Override public String toString() { return "GiniCaptureDocument{" @@ -309,6 +320,7 @@ public boolean equals(final Object o) { if (mImportMethod != that.mImportMethod) { return false; } + return mMimeType.equals(that.mMimeType); } diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/document/UploadMetadata.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/document/UploadMetadata.kt new file mode 100644 index 0000000000..70132ab8bd --- /dev/null +++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/document/UploadMetadata.kt @@ -0,0 +1,84 @@ +package net.gini.android.capture.document + +import android.os.Build +import net.gini.android.capture.BuildConfig +import net.gini.android.capture.EntryPoint +import net.gini.android.capture.GiniCapture + +internal object UploadMetadata { + + private const val USER_COMMENT_PLATFORM = "Platform" + private const val USER_COMMENT_OS_VERSION = "OSVer" + private const val USER_COMMENT_GINI_CAPTURE_VERSION = "GiniVisionVer" + private const val USER_COMMENT_DEVICE_ORIENTATION = "DeviceOrientation" + private const val USER_COMMENT_DEVICE_TYPE = "DeviceType" + private const val USER_COMMENT_SOURCE = "Source" + private const val USER_COMMENT_IMPORT_METHOD = "ImportMethod" + private const val USER_COMMENT_ENTRY_POINT = "EntryPoint" + + private var giniCaptureVersion: String = "" + private var deviceOrientation: String = "" + private var deviceType: String = "" + private var source: String = "" + private var importMethod: String = "" + + private fun convertMapToCSV(keyValueMap: Map): String { + val csvBuilder = StringBuilder() + var isFirst = true + for ((key, value) in keyValueMap) { + if (!isFirst) { + csvBuilder.append(',') + } + isFirst = false + csvBuilder.append(key) + .append('=') + .append(value) + } + return csvBuilder.toString() + } + + fun setDeviceOrientation(deviceOrientation: String): UploadMetadata = + this.also { it.deviceOrientation = deviceOrientation } + + fun setDeviceType(deviceType: String): UploadMetadata = + this.also { it.deviceType = deviceType } + + fun setSource(source: String): UploadMetadata = this.also { it.source = source } + + fun setImportMethod(importMethod: String): UploadMetadata = this.also { it.importMethod = importMethod } + + fun build(): String { + val metadataMap = mutableMapOf() + + metadataMap[USER_COMMENT_PLATFORM] = "Android" + metadataMap[USER_COMMENT_OS_VERSION] = Build.VERSION.RELEASE.toString() + if (giniCaptureVersion.isNotEmpty()) { + metadataMap[USER_COMMENT_GINI_CAPTURE_VERSION] = giniCaptureVersion + } + if (deviceOrientation.isNotEmpty()) { + metadataMap[USER_COMMENT_DEVICE_ORIENTATION] = deviceOrientation + } + if (deviceType.isNotEmpty()) { + metadataMap[USER_COMMENT_DEVICE_TYPE] = deviceType + } + if (source.isNotEmpty()) { + metadataMap[USER_COMMENT_SOURCE] = source + } + if (importMethod.isNotEmpty()) { + metadataMap[USER_COMMENT_IMPORT_METHOD] = importMethod + } + metadataMap[USER_COMMENT_GINI_CAPTURE_VERSION] = BuildConfig.VERSION_NAME.replace(" ", "") + + if (GiniCapture.hasInstance()) { + metadataMap[USER_COMMENT_ENTRY_POINT] = entryPointToString(GiniCapture.getInstance().getEntryPoint()) + } else { + metadataMap[USER_COMMENT_ENTRY_POINT] = entryPointToString(GiniCapture.Internal.DEFAULT_ENTRY_POINT) + } + return convertMapToCSV(metadataMap) + } + + private fun entryPointToString(entryPoint: EntryPoint) = when (entryPoint) { + EntryPoint.FIELD -> "field" + EntryPoint.BUTTON -> "button" + } +} diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentMetadata.java b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentMetadata.java index b14f6ef10d..5343a92276 100644 --- a/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentMetadata.java +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/DocumentMetadata.java @@ -1,14 +1,14 @@ package net.gini.android.core.api; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.util.HashMap; import java.util.Map; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - /** * Created by Alpar Szotyori on 25.10.2018. * @@ -26,6 +26,8 @@ public class DocumentMetadata { public static final String HEADER_FIELD_NAME_PREFIX = "X-Document-Metadata-"; @VisibleForTesting public static final String BRANCH_ID_HEADER_FIELD_NAME = HEADER_FIELD_NAME_PREFIX + "BranchId"; + @VisibleForTesting + public static final String UPLOAD_METADATA_HEADER_FIELD_NAME = HEADER_FIELD_NAME_PREFIX + "Upload"; private final Map mMetadataMap = new HashMap<>(); @@ -63,6 +65,19 @@ public void setBranchId(@NonNull final String branchId) throws IllegalArgumentEx } } + /** + * Set upload metadata to be sent to backend + * + * @param uploadMetadata containing info related to the device, file import type etc... + */ + public void setUploadMetadata(@NonNull final String uploadMetadata) { + if (isASCIIEncodable(uploadMetadata)) { + mMetadataMap.put(UPLOAD_METADATA_HEADER_FIELD_NAME, uploadMetadata); + } else { + throw new IllegalArgumentException("Metadata is not encodable as ASCII: " + uploadMetadata); + } + } + @VisibleForTesting public boolean isASCIIEncodable(@NonNull final String string) { if (mAsciiCharsetEncoder != null) { @@ -98,6 +113,17 @@ public void add(@NonNull final String name, @NonNull final String value) mMetadataMap.put(completeName, value); } + /** + * Provides a copy of the [DocumentMetadata] object + * + * @return the copy of the metadata object + */ + public DocumentMetadata copy() { + DocumentMetadata copy = new DocumentMetadata(); + mMetadataMap.forEach((key, value) -> copy.add(key, value)); + return copy; + } + @NonNull @VisibleForTesting public Map getMetadata() {