From 9f758af0822293e04a35df01b2acefda40163b5a Mon Sep 17 00:00:00 2001 From: Dmytro Kos Date: Tue, 20 Oct 2020 10:40:34 +0300 Subject: [PATCH 01/13] add new card scanner solution --- app/build.gradle | 1 + .../activity_case/VGSCollectActivity.kt | 27 ++- settings.gradle | 2 +- vgscollect-bouncer/README.md | 72 ++++++ vgscollect-bouncer/build.gradle | 34 +++ vgscollect-bouncer/consumer-rules.pro | 0 vgscollect-bouncer/proguard-rules.pro | 21 ++ .../src/main/AndroidManifest.xml | 9 + .../api/bouncer/ScanActivity.kt | 205 ++++++++++++++++++ 9 files changed, 362 insertions(+), 9 deletions(-) create mode 100644 vgscollect-bouncer/README.md create mode 100644 vgscollect-bouncer/build.gradle create mode 100644 vgscollect-bouncer/consumer-rules.pro create mode 100644 vgscollect-bouncer/proguard-rules.pro create mode 100644 vgscollect-bouncer/src/main/AndroidManifest.xml create mode 100644 vgscollect-bouncer/src/main/java/com/verygoodsecurity/api/bouncer/ScanActivity.kt diff --git a/app/build.gradle b/app/build.gradle index 659a7b285..8fe914ca2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -76,6 +76,7 @@ dependencies { implementation project(":vgscollect") implementation project(":vgscollect-cardio") + implementation project(":vgscollect-bouncer") implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' diff --git a/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt b/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt index 3f33367a5..0c05420a9 100644 --- a/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt +++ b/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt @@ -6,10 +6,13 @@ import android.graphics.Rect import android.graphics.drawable.Drawable import android.os.Bundle import android.util.Log -import android.view.* +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat -import com.verygoodsecurity.api.cardio.ScanActivity +import com.verygoodsecurity.api.bouncer.ScanActivity import com.verygoodsecurity.demoapp.R import com.verygoodsecurity.demoapp.StartActivity import com.verygoodsecurity.vgscollect.core.Environment @@ -62,7 +65,7 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. val staticData = mutableMapOf() staticData["static_data"] = "static custom data" -// vgsForm.setCustomData(staticData) + vgsForm.setCustomData(staticData) } private fun setupCardExpDateField() { @@ -243,10 +246,18 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. this[cardHolderField?.getFieldName()] = ScanActivity.CARD_HOLDER this[cardExpDateField?.getFieldName()] = ScanActivity.CARD_EXP_DATE } + putSerializable(ScanActivity.SCAN_CONFIGURATION, scanSettings) - putInt(ScanActivity.EXTRA_GUIDE_COLOR, Color.WHITE) - putString(ScanActivity.EXTRA_LANGUAGE_OR_LOCALE, "en") - putString(ScanActivity.EXTRA_SCAN_INSTRUCTIONS, "Scanning payment card") + + putString(ScanActivity.API_KEY, "") + + putBoolean(ScanActivity.ENABLE_EXPIRY_EXTRACTION, false) + putBoolean(ScanActivity.ENABLE_NAME_EXTRACTION, false) + putBoolean(ScanActivity.DISPLAY_CARD_PAN, false) + putBoolean(ScanActivity.DISPLAY_CARD_HOLDER_NAME, false) + putBoolean(ScanActivity.DISPLAY_CARD_SCAN_LOGO, false) + putBoolean(ScanActivity.ENABLE_DEBUG, false) + this } @@ -357,8 +368,8 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. val request: VGSRequest = VGSRequest.VGSRequestBuilder() .setMethod(HTTPMethod.POST) .setPath(path) -// .setCustomHeader(headers) -// .setCustomData(customData) + .setCustomHeader(headers) + .setCustomData(customData) .build() vgsForm.asyncSubmit(request) diff --git a/settings.gradle b/settings.gradle index 1df1eea0f..716e1c455 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':vgscollect', ':vgscollect-cardio' \ No newline at end of file +include ':app', ':vgscollect', ':vgscollect-cardio', ':vgscollect-bouncer' \ No newline at end of file diff --git a/vgscollect-bouncer/README.md b/vgscollect-bouncer/README.md new file mode 100644 index 000000000..537a352d1 --- /dev/null +++ b/vgscollect-bouncer/README.md @@ -0,0 +1,72 @@ +# VGSCollect-Bouncer + +Module based on [Bouncer](https://github.com/getbouncer/cardscan-android). It allows to secure integrate scanner with [VGSCollect](https://github.com/verygoodsecurity/vgs-collect-android) + +Table of contents +================= + + + * [Dependencies](#dependencies) + * [Integration](#integration) + * [Add the SDK to your project](#add-the-sdk-to-your-project) + * [Usage](#usage) + + +## Dependencies + +| Dependency | Version | +| :--- | :---: | +| Min SDK | 21 | +| androidx.appcompat:appcompat | 1.2.1 | +| androidx.core:core-ktx | 1.2.1 | + +## Integration +For integration you need to install the [Android Studio](http://developer.android.com/sdk/index.html) and a [JDK](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) on your machine. + +#### Add the SDK to your project +To use the SDK in project you just simply need to add the following line of dependency in your module `gradle.gradle` file: +``` +dependencies { + implementation 'com.verygoodsecurity:vgscollect:1.2.5’ //required version 1.2.5 or above + implementation 'com.verygoodsecurity.api:adapter-bouncer:’ +} +``` + +## Usage + +**Important**: VGSCollect should be configured too. Check [here](https://www.verygoodsecurity.com/docs/vgs-collect/android-sdk#step-2-configure-your-app) how to do it. + +Before scanning you have to setup which information you need to retrieve and final destination VGS secure field: +``` +HashMap scanSettings = new HashMap<>(); +scanSettings.put(cardNumberField.getFieldName(), ScanActivity.CARD_NUMBER); +scanSettings.put(cardCvcield.getFieldName(), ScanActivity.CARD_CVC); +scanSettings.put(cardHolderField.getFieldName(), ScanActivity.CARD_HOLDER); +scanSettings.put(cardExpDateField.getFieldName(), ScanActivity.CARD_EXP_DATE); +``` + +To start scanning you need to attach a Map with settings to Intent and start ScanActivity. +``` +public void scanCard() { + HashMap scanSettings = new HashMap<>(); + String numberKey = cardNumberField.getFieldName(); + scanSettings.put(numberKey, ScanActivity.CARD_NUMBER); + + Intent intent = new Intent(this, ScanActivity.class); + intent.putExtra(ScanActivity.SCAN_CONFIGURATION, scanSettings); + + intent.putString(ScanActivity.API_KEY, "") + + startActivityForResult(intent, USER_SCAN_REQUEST_CODE); +} +``` + +Also very important to call [VGSCollect](https://github.com/verygoodsecurity/vgs-collect-android) **onActivityResult** method inside Activity onActivityResult callback: +``` +@Override +public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + vgsForm.onActivityResult(requestCode, resultCode, data); + } + +``` diff --git a/vgscollect-bouncer/build.gradle b/vgscollect-bouncer/build.gradle new file mode 100644 index 000000000..6910f95c3 --- /dev/null +++ b/vgscollect-bouncer/build.gradle @@ -0,0 +1,34 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 30 + buildToolsVersion "29.0.3" + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "androidx.appcompat:appcompat:$android_support_libraries" + implementation 'androidx.core:core-ktx:1.3.1' + + api project(':vgscollect') + implementation 'com.getbouncer:cardscan-ui:2.0.0018' +} \ No newline at end of file diff --git a/vgscollect-bouncer/consumer-rules.pro b/vgscollect-bouncer/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/vgscollect-bouncer/proguard-rules.pro b/vgscollect-bouncer/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/vgscollect-bouncer/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/vgscollect-bouncer/src/main/AndroidManifest.xml b/vgscollect-bouncer/src/main/AndroidManifest.xml new file mode 100644 index 000000000..f66e71c83 --- /dev/null +++ b/vgscollect-bouncer/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/vgscollect-bouncer/src/main/java/com/verygoodsecurity/api/bouncer/ScanActivity.kt b/vgscollect-bouncer/src/main/java/com/verygoodsecurity/api/bouncer/ScanActivity.kt new file mode 100644 index 000000000..e22c149ba --- /dev/null +++ b/vgscollect-bouncer/src/main/java/com/verygoodsecurity/api/bouncer/ScanActivity.kt @@ -0,0 +1,205 @@ +package com.verygoodsecurity.api.bouncer + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.util.Log +import com.getbouncer.cardscan.ui.CardScanActivity +import com.getbouncer.cardscan.ui.CardScanActivityResult +import com.getbouncer.cardscan.ui.CardScanActivityResultHandler +import com.getbouncer.scan.framework.exception.InvalidBouncerApiKeyException +import com.verygoodsecurity.vgscollect.BuildConfig +import com.verygoodsecurity.vgscollect.app.BaseTransmitActivity +import java.text.SimpleDateFormat +import java.util.* + +class ScanActivity: BaseTransmitActivity(), CardScanActivityResultHandler { + + private lateinit var key:String + + private lateinit var settings:Map + + private var enableEnterCardManually = false + private var enableExpiryExtraction = false + private var enableNameExtraction = false + private var displayCardPan = false + private var displayCardholderName = false + private var displayCardScanLogo = false + private var enableDebug = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + saveSettings() + + try { + val initializeNameAndExpiryExtraction = enableNameExtraction || enableExpiryExtraction + CardScanActivity.warmUp(this, key, initializeNameAndExpiryExtraction) + } catch (e: InvalidBouncerApiKeyException) { + printLog(e) + finish() + } + } + + override fun onStart() { + super.onStart() + runCardIO() + } + + private fun saveSettings() { + intent.extras?.let { + settings = it.getSerializable(SCAN_CONFIGURATION)?.run { + this as HashMap + }?:HashMap() + key = it.getString(API_KEY, "") + + enableExpiryExtraction = it.getBoolean(ENABLE_EXPIRY_EXTRACTION) + enableNameExtraction = it.getBoolean(ENABLE_NAME_EXTRACTION) + displayCardPan = it.getBoolean(DISPLAY_CARD_PAN) + displayCardholderName = it.getBoolean(DISPLAY_CARD_HOLDER_NAME) + displayCardScanLogo = it.getBoolean(DISPLAY_CARD_SCAN_LOGO) + enableDebug = it.getBoolean(ENABLE_DEBUG) + } + } + + private fun runCardIO() { + CardScanActivity.start(this, + key, + enableEnterCardManually, + enableExpiryExtraction, + enableNameExtraction, + displayCardPan, + displayCardholderName, + displayCardScanLogo, + enableDebug + ) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + CardScanActivity.parseScanResult(resultCode, data, this) + finish() + } + + override fun analyzerFailure(scanId: String?) {} + + override fun cameraError(scanId: String?) {} + + override fun canceledUnknown(scanId: String?) {} + + override fun cardScanned(scanId: String?, scanResult: CardScanActivityResult) { + settings.forEach { + when(it.value) { + CARD_NUMBER -> mapData(it.key, scanResult.pan) + CARD_CVC -> mapData(it.key, scanResult.cvc) + CARD_HOLDER -> mapData(it.key, scanResult.cardholderName) + CARD_EXP_DATE -> mapData(it.key, retrieveDate(scanResult)) + } + } + } + + private fun retrieveDate(scanResult: CardScanActivityResult): Long? { + val day:Int + val mounts:Int + val year:Int + + scanResult.also { + day = it.expiryDay?.toIntOrNull()?:0 + }.also { + mounts = it.expiryMonth?.toIntOrNull()?:0 + }.also { + year = it.expiryYear?.toIntOrNull()?:0 + } + + return if(mounts != 0 && year != 0) { + val dStr:String + val dMask:String + if(day != 0) { + dStr = String.format("%02d", day)+"/" + dMask = day.toString().replace("\\d".toRegex(), "d")+"/" + } else { + dStr = "" + dMask = "" + } + + + val mMask = String.format("%02d", mounts) + .replace("\\d".toRegex(), "M")+"/" + + val yMask = mounts.toString() + .replace("\\d".toRegex(), "y") + + val mStr = String.format("%02d", mounts)+"/" + val yStr = year.toString() + val date = SimpleDateFormat("$dMask$mMask$yMask", Locale.getDefault()) + .parse("$dStr$mStr$yStr") + date?.time + } else { + null + } + } + + override fun enterManually(scanId: String?) {} + + override fun userCanceled(scanId: String?) {} + + companion object { + private const val NAME = "ScanActivity" + const val SCAN_CONFIGURATION = "vgs_scan_settings" + + const val CARD_NUMBER = 0x71 + const val CARD_CVC = 0x72 + const val CARD_HOLDER = 0x73 + const val CARD_EXP_DATE = 0x74 + + /** + The bouncer API key used to run scanning. + */ + const val API_KEY = "apikey" + + /** + If true, attempt to extract the card expiry. + */ + const val ENABLE_EXPIRY_EXTRACTION = "enableExpiryExtraction" + + /** + If true, attempt to extract the cardholder name. + */ + const val ENABLE_NAME_EXTRACTION = "enableNameExtraction" + + /** + If true, display the card pan once the card has started to scan. + */ + const val DISPLAY_CARD_PAN = "displayCardPan" + + /** + If true, display the name of the card owner if extracted. + */ + const val DISPLAY_CARD_HOLDER_NAME = "displayCardholderName" + + /** + If true, display the cardscan.io logo at the top of the screen. + */ + const val DISPLAY_CARD_SCAN_LOGO = "displayCardScanLogo" + + /** + If true, enable debug views in card scan. + */ + const val ENABLE_DEBUG = "enableDebug" + + /** + Start the card scanner activity. + */ + fun scan(context:Activity, code:Int, bndl:Bundle = Bundle.EMPTY) { + val intent = Intent(context, ScanActivity::class.java) + intent.putExtras(bndl) + context.startActivityForResult(intent, code) + } + } + + private fun printLog(e: Exception) { + if (BuildConfig.DEBUG) { + Log.e(NAME, e.message?:"", e) + } + } +} \ No newline at end of file From 008a48239f361f103e98aed08ac646f2e8eebef6 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Tue, 20 Oct 2020 17:13:40 +0300 Subject: [PATCH 02/13] update version to CardScan --- vgscollect-bouncer/build.gradle | 4 ++-- vgscollect-bouncer/gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vgscollect-bouncer/build.gradle b/vgscollect-bouncer/build.gradle index c06cd651d..c3a3a0e59 100644 --- a/vgscollect-bouncer/build.gradle +++ b/vgscollect-bouncer/build.gradle @@ -10,8 +10,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 30 - versionCode 1 - versionName "1.0" + versionCode CODE_VERSION.toInteger() + versionName POM_VERSION testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" diff --git a/vgscollect-bouncer/gradle.properties b/vgscollect-bouncer/gradle.properties index 8c6c52ec0..178b0d701 100644 --- a/vgscollect-bouncer/gradle.properties +++ b/vgscollect-bouncer/gradle.properties @@ -3,5 +3,5 @@ POM_DESCRIPTION=CardScan - fast and modern SDK for scanning Card Numbers. Integr POM_BINTRAY_NAME=adapter-bouncer POM_ARTIFACT_ID=adapter-bouncer POM_PACKAGING=aar -POM_VERSION=0.0.1 -CODE_VERSION=1 \ No newline at end of file +POM_VERSION=1.0.0 +CODE_VERSION=100 \ No newline at end of file From 480f140211a1983382dd5fe974cf6a43c6d4be58 Mon Sep 17 00:00:00 2001 From: DmytroDm <72735363+DmytroDm@users.noreply.github.com> Date: Fri, 23 Oct 2020 13:00:02 +0300 Subject: [PATCH 03/13] Upgrade gradle version, fix gradle behavior changes in new version(https://developer.android.com/studio/releases/gradle-plugin.html#updating-gradle) (#42) --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 5 +++-- vgscollect/build.gradle | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index b9c3ab6f0..126814ac3 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 070cf7caf..acd884c8c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Thu Oct 22 13:07:50 EEST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/vgscollect/build.gradle b/vgscollect/build.gradle index 750ddfe5f..d2983f623 100644 --- a/vgscollect/build.gradle +++ b/vgscollect/build.gradle @@ -13,6 +13,8 @@ android { versionCode CODE_VERSION.toInteger() versionName POM_VERSION + buildConfigField "String", "VERSION_NAME", "\"$POM_VERSION\"" + vectorDrawables.useSupportLibrary = true javaCompileOptions.annotationProcessorOptions.includeCompileClasspath = true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From d6a466404631573cd801774a6080b819ade1cc4d Mon Sep 17 00:00:00 2001 From: Dmytro Kos Date: Tue, 3 Nov 2020 12:13:04 +0300 Subject: [PATCH 04/13] make collect more flexible for initialization --- .../verygoodsecurity/vgscollect/core/VGSCollect.kt | 14 ++++++++++++++ .../vgscollect/util/UriExtension.kt | 10 +++++++++- .../vgscollect/UrlExtensionTest.kt | 10 ++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt index 8ee8d7358..07898dfb5 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt @@ -94,6 +94,20 @@ class VGSCollect { initializeCollect(baseURL) } + constructor( + /** Activity context */ + context: Context, + + /** Unique Vault id */ + id: String, + + /** Type of Environment */ + environmentType: String, + + /** Region identifier */ + suffix: String + ): this(context, id, environmentType with suffix) + constructor( /** Activity context */ context: Context, diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/UriExtension.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/UriExtension.kt index b92767bea..e3362a249 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/UriExtension.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/UriExtension.kt @@ -6,7 +6,7 @@ import android.provider.OpenableColumns import com.verygoodsecurity.vgscollect.core.model.state.FileState /** @suppress */ -internal fun Uri.parseFile(context: Context, fieldName:String): FileState? { +internal fun Uri.parseFile(context: Context, fieldName: String): FileState? { val mimeType: String? = this.let { returnUri -> context.contentResolver.getType(returnUri) } @@ -28,4 +28,12 @@ internal fun Uri.parseFile(context: Context, fieldName:String): FileState? { mimeType, fieldName ) +} + +internal infix fun String.with(suffix: String): String { + return when { + suffix.isEmpty() -> this + suffix[0] == '-' -> this + suffix + else -> "$this-$suffix" + } } \ No newline at end of file diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/UrlExtensionTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/UrlExtensionTest.kt index 2f9d37509..3d7c7f2ec 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/UrlExtensionTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/UrlExtensionTest.kt @@ -2,6 +2,7 @@ package com.verygoodsecurity.vgscollect import com.verygoodsecurity.vgscollect.core.Environment import com.verygoodsecurity.vgscollect.core.api.* +import com.verygoodsecurity.vgscollect.util.with import org.junit.Assert.* import org.junit.Test import java.util.regex.Pattern @@ -218,4 +219,13 @@ class UrlExtensionTest { val url5 = tennant.setupURL("SANDBOX-EU-1") assertTrue(Pattern.compile(URL_REGEX).matcher(url5).matches()) } + + @Test + fun test_environment_concatenation() { + assertEquals("sandbox-eu-1", "sandbox" with "eu-1") + assertEquals("sandbox-eu-1", "sandbox" with "-eu-1") + assertEquals("live-eu-", "live" with "-eu-") + assertEquals("live-eu", "live" with "eu") + assertEquals("live", "live" with "") + } } \ No newline at end of file From 00b603a192e0f7522ed4cd8bd997998c87318479 Mon Sep 17 00:00:00 2001 From: Dmytro Kos Date: Mon, 23 Nov 2020 18:00:32 +0300 Subject: [PATCH 05/13] Refactored HttpClient (#44) --- .../activity_case/VGSCollectActivity.kt | 31 +- .../main/res/layout/activity_collect_demo.xml | 4 + .../vgscollect/core/HTTPMethod.kt | 5 - .../vgscollect/core/VGSCollect.kt | 299 +++++++----------- .../vgscollect/core/api/AnalyticClient.kt | 129 -------- .../core/api/AnalyticURLConnectionClient.kt | 112 ------- .../vgscollect/core/api/ApiClient.kt | 15 - .../vgscollect/core/api/OkHttpClient.kt | 187 ----------- .../core/api/URLConnectionClient.kt | 155 --------- .../vgscollect/core/api/UrlExtension.kt | 27 -- .../vgscollect/core/api/VGSHttpBodyFormat.kt | 9 + .../core/api/analityc/CollectActionTracker.kt | 46 ++- .../vgscollect/core/api/client/ApiClient.kt | 41 +++ .../core/api/client/OkHttpClient.kt | 212 +++++++++++++ .../core/api/client/URLConnectionClient.kt | 150 +++++++++ .../api/client/extension/HttpUrlConnection.kt | 65 ++++ .../api/client/extension/OkHttpConnection.kt | 19 ++ .../core/api/client/extension/Response.kt | 9 + .../api/{ => client/ssl}/TLSSocketFactory.kt | 2 +- .../vgscollect/core/api/doAsync.kt | 32 -- .../core/model/network/NetworkResponse.kt | 30 ++ .../vgscollect/core/model/network/VGSError.kt | 11 + .../core/model/network/VGSRequest.kt | 37 ++- .../core/model/network/VGSResponse.kt | 18 +- .../core/model/state/FieldContent.kt | 2 +- .../content/file/TemporaryFileStorage.kt | 2 +- .../vgscollect/util/BooleanExtension.kt | 10 - .../vgscollect/util/CommonUtil.kt | 34 -- .../util/extension/NetworkConnection.kt | 26 ++ .../Number.kt} | 2 +- .../vgscollect/util/extension/String.kt | 16 + .../{UriExtension.kt => extension/Uri.kt} | 10 +- .../view/internal/CardInputField.kt | 2 +- .../vgscollect/NumberExtensionTest.kt | 2 +- .../vgscollect/TestApplication.kt | 4 +- .../vgscollect/UrlExtensionTest.kt | 46 --- .../vgscollect/VGSCollectTest.kt | 235 +++++++++++--- .../vgscollect/api/ApiClientTest.kt | 37 ++- .../vgscollect/api/RequestTest.kt | 10 +- .../vgscollect/api/TLSSocketFactoryTest.kt | 2 +- .../api/client/extension/ResponseTest.kt | 40 +++ .../vgscollect/utils/extension/StringTest.kt | 27 ++ 42 files changed, 1084 insertions(+), 1068 deletions(-) delete mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/AnalyticClient.kt delete mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/AnalyticURLConnectionClient.kt delete mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/ApiClient.kt delete mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/OkHttpClient.kt delete mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/URLConnectionClient.kt create mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/VGSHttpBodyFormat.kt create mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/ApiClient.kt create mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/OkHttpClient.kt create mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/URLConnectionClient.kt create mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/extension/HttpUrlConnection.kt create mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/extension/OkHttpConnection.kt create mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/extension/Response.kt rename vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/{ => client/ssl}/TLSSocketFactory.kt (98%) delete mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/doAsync.kt create mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/NetworkResponse.kt delete mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/BooleanExtension.kt delete mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/CommonUtil.kt create mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/extension/NetworkConnection.kt rename vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/{NumberExtension.kt => extension/Number.kt} (64%) create mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/extension/String.kt rename vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/{UriExtension.kt => extension/Uri.kt} (79%) create mode 100644 vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/client/extension/ResponseTest.kt create mode 100644 vgscollect/src/test/java/com/verygoodsecurity/vgscollect/utils/extension/StringTest.kt diff --git a/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt b/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt index 880e55df0..5a8ffda86 100644 --- a/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt +++ b/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt @@ -11,7 +11,7 @@ import android.view.MenuItem import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat -import com.verygoodsecurity.api.bouncer.ScanActivity +import com.verygoodsecurity.api.cardio.ScanActivity import com.verygoodsecurity.demoapp.R import com.verygoodsecurity.demoapp.StartActivity import com.verygoodsecurity.vgscollect.core.Environment @@ -238,29 +238,18 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. } private fun scanCard() { - val bndl = with(Bundle()) { - val scanSettings = hashMapOf().apply { - this[cardNumberField?.getFieldName()] = ScanActivity.CARD_NUMBER - this[cardCVCField?.getFieldName()] = ScanActivity.CARD_CVC - this[cardHolderField?.getFieldName()] = ScanActivity.CARD_HOLDER - this[cardExpDateField?.getFieldName()] = ScanActivity.CARD_EXP_DATE - } - - putSerializable(ScanActivity.SCAN_CONFIGURATION, scanSettings) - - putString(ScanActivity.API_KEY, "") + val intent = Intent(this, ScanActivity::class.java) - putBoolean(ScanActivity.ENABLE_EXPIRY_EXTRACTION, false) - putBoolean(ScanActivity.ENABLE_NAME_EXTRACTION, false) - putBoolean(ScanActivity.DISPLAY_CARD_PAN, false) - putBoolean(ScanActivity.DISPLAY_CARD_HOLDER_NAME, false) - putBoolean(ScanActivity.DISPLAY_CARD_SCAN_LOGO, false) - putBoolean(ScanActivity.ENABLE_DEBUG, false) - - this + val scanSettings = hashMapOf().apply { + this[cardNumberField?.getFieldName()] = ScanActivity.CARD_NUMBER + this[cardCVCField?.getFieldName()] = ScanActivity.CARD_CVC + this[cardHolderField?.getFieldName()] = ScanActivity.CARD_HOLDER + this[cardExpDateField?.getFieldName()] = ScanActivity.CARD_EXP_DATE } - ScanActivity.scan(this, USER_SCAN_REQUEST_CODE, bndl) + intent.putExtra(ScanActivity.SCAN_CONFIGURATION, scanSettings) + + startActivityForResult(intent, USER_SCAN_REQUEST_CODE) } private fun getOnFieldStateChangeListener(): OnFieldStateChangeListener { diff --git a/app/src/main/res/layout/activity_collect_demo.xml b/app/src/main/res/layout/activity_collect_demo.xml index 333835779..c9a283eab 100644 --- a/app/src/main/res/layout/activity_collect_demo.xml +++ b/app/src/main/res/layout/activity_collect_demo.xml @@ -138,6 +138,7 @@ android:layout_height="match_parent" app:fieldName="card_data.cardNumber" style="@style/AppTheme.PaymentField" + app:text="4111-1111-1111-1111" app:numberDivider="-" app:validationRule="acceptUnknown" app:fontFamily="@font/robotomono_light" @@ -166,6 +167,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" style="@style/AppTheme.PaymentField" + app:text="John G" android:nextFocusDown="@+id/cardExpDateField" app:imeOptions="actionNext" app:fontFamily="@font/robotomono_light" @@ -197,6 +199,7 @@ android:nextFocusDown="@+id/cardCVCField" app:imeOptions="actionNext" app:inputType="date" + app:text="09/2025" app:fontFamily="@font/robotomono_light" app:datePickerModes="input" app:outputPattern="yyyy-MM-dd'T'HH:mm:ss.SSSSSSS" @@ -217,6 +220,7 @@ android:layout_height="match_parent" style="@style/AppTheme.PaymentField" app:fieldName="card_data.cardCvc" + app:text="123" app:fontFamily="@font/robotomono_light" app:imeOptions="actionDone"/> diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/HTTPMethod.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/HTTPMethod.kt index cf6870b41..c2894e207 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/HTTPMethod.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/HTTPMethod.kt @@ -7,11 +7,6 @@ package com.verygoodsecurity.vgscollect.core */ enum class HTTPMethod { - /** - * HTTP GET method - */ - GET, - /** * HTTP POST method */ diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt index 07898dfb5..329c886d5 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt @@ -3,20 +3,18 @@ package com.verygoodsecurity.vgscollect.core import android.app.Activity import android.content.Context import android.content.Intent -import android.content.pm.PackageManager -import android.os.AsyncTask +import android.os.Handler +import android.os.Looper import androidx.annotation.VisibleForTesting -import androidx.core.content.ContextCompat import com.verygoodsecurity.vgscollect.app.BaseTransmitActivity import com.verygoodsecurity.vgscollect.core.api.* import com.verygoodsecurity.vgscollect.core.api.analityc.CollectActionTracker import com.verygoodsecurity.vgscollect.core.api.analityc.AnalyticTracker import com.verygoodsecurity.vgscollect.core.api.analityc.action.* -import com.verygoodsecurity.vgscollect.core.model.* +import com.verygoodsecurity.vgscollect.core.api.client.ApiClient +import com.verygoodsecurity.vgscollect.core.api.client.extension.isHttpStatusCode import com.verygoodsecurity.vgscollect.core.model.VGSHashMapWrapper -import com.verygoodsecurity.vgscollect.core.model.network.VGSError -import com.verygoodsecurity.vgscollect.core.model.network.VGSRequest -import com.verygoodsecurity.vgscollect.core.model.network.VGSResponse +import com.verygoodsecurity.vgscollect.core.model.network.* import com.verygoodsecurity.vgscollect.core.model.state.FieldState import com.verygoodsecurity.vgscollect.core.model.state.mapToFieldState import com.verygoodsecurity.vgscollect.core.storage.* @@ -27,6 +25,10 @@ import com.verygoodsecurity.vgscollect.core.storage.external.DependencyReceiver import com.verygoodsecurity.vgscollect.core.storage.external.ExternalDependencyDispatcher import com.verygoodsecurity.vgscollect.util.* import com.verygoodsecurity.vgscollect.util.Logger +import com.verygoodsecurity.vgscollect.util.extension.concatWithDash +import com.verygoodsecurity.vgscollect.util.extension.hasAccessNetworkStatePermission +import com.verygoodsecurity.vgscollect.util.extension.hasInternetPermission +import com.verygoodsecurity.vgscollect.util.extension.isConnectionAvailable import com.verygoodsecurity.vgscollect.util.mapUsefulPayloads import com.verygoodsecurity.vgscollect.view.InputFieldView import com.verygoodsecurity.vgscollect.view.card.getAnalyticName @@ -34,7 +36,6 @@ import java.security.MessageDigest import java.util.* import kotlin.collections.HashMap - /** * VGS Collect allows you to securely collect data and files from your users without having * to have them pass through your systems. @@ -46,29 +47,39 @@ class VGSCollect { private val externalDependencyDispatcher: ExternalDependencyDispatcher - private val tracker:AnalyticTracker + private val tracker: AnalyticTracker private lateinit var client: ApiClient - - private lateinit var storage:InternalStorage - private var storageErrorListener:StorageErrorListener + private val mainHandler: Handler = Handler(Looper.getMainLooper()) + + private lateinit var storage: InternalStorage + private val storageErrorListener: StorageErrorListener = object : StorageErrorListener { + override fun onStorageError(error: VGSError) { + VGSError.INPUT_DATA_NOT_VALID.toVGSResponse(context).also { r -> + notifyAllListeners(r) + Logger.e(VGSCollect::class.java, r.localizeMessage) + submitEvent(false, code = r.errorCode) + } + } + } private val responseListeners = mutableListOf() - private val analyticListener = object :VgsCollectResponseListener { + private val analyticListener = object : VgsCollectResponseListener { override fun onResponse(response: VGSResponse?) { - when(response) { - is VGSResponse.ErrorResponse -> responseEvent(response.code, response.localizeMessage) + when (response) { + is VGSResponse.ErrorResponse -> responseEvent( + response.code, + response.localizeMessage + ) is VGSResponse.SuccessResponse -> responseEvent(response.code) } } } - private var currentTask:AsyncTask? = null - - private val baseURL:String + private val baseURL: String private val context: Context - private val isURLValid:Boolean + private val isURLValid: Boolean constructor( /** Activity context */ @@ -83,11 +94,10 @@ class VGSCollect { this.context = context tracker = CollectActionTracker( - context, id, environment.rawValue, - UUID.randomUUID().toString()) - + UUID.randomUUID().toString() + ) baseURL = id.setupURL(environment.rawValue) isURLValid = baseURL.isURLValid() @@ -106,7 +116,7 @@ class VGSCollect { /** Region identifier */ suffix: String - ): this(context, id, environmentType with suffix) + ) : this(context, id, environmentType concatWithDash suffix) constructor( /** Activity context */ @@ -120,31 +130,25 @@ class VGSCollect { ) { this.context = context tracker = CollectActionTracker( - context, id, environment, - UUID.randomUUID().toString()) + UUID.randomUUID().toString() + ) baseURL = id.setupURL(environment) isURLValid = baseURL.isURLValid() initializeCollect(baseURL) - } private fun initializeCollect(baseURL: String) { - client = OkHttpClient.newInstance(context, baseURL) + client = ApiClient.newHttpClient() + client.setURL(baseURL) storage = InternalStorage(context, storageErrorListener) } init { externalDependencyDispatcher = DependencyReceiver() - - storageErrorListener = object : StorageErrorListener { - override fun onStorageError(error: VGSError) { - notifyErrorResponse(error) - } - } addOnResponseListeners(analyticListener) } @@ -153,9 +157,9 @@ class VGSCollect { * * @param onResponseListener Interface definition for a receiving callback. */ - fun addOnResponseListeners(onResponseListener:VgsCollectResponseListener?) { + fun addOnResponseListeners(onResponseListener: VgsCollectResponseListener?) { onResponseListener?.let { - if(!responseListeners.contains(it)) responseListeners.add(it) + if (!responseListeners.contains(it)) responseListeners.add(it) } } @@ -172,8 +176,10 @@ class VGSCollect { * * @param onResponseListener Interface definition for a receiving callback. */ - fun removeOnResponseListener(onResponseListener:VgsCollectResponseListener) { - if(responseListeners.contains(onResponseListener)) responseListeners.remove(onResponseListener) + fun removeOnResponseListener(onResponseListener: VgsCollectResponseListener) { + if (responseListeners.contains(onResponseListener)) responseListeners.remove( + onResponseListener + ) } /** @@ -183,7 +189,10 @@ class VGSCollect { */ fun bindView(view: InputFieldView?) { view?.statePreparer?.let { - externalDependencyDispatcher.addDependencyListener(view.getFieldName(), it.getDependencyListener()) + externalDependencyDispatcher.addDependencyListener( + view.getFieldName(), + it.getDependencyListener() + ) it.setAnalyticTracker(tracker) } @@ -198,7 +207,7 @@ class VGSCollect { * * @param fieldStateListener listener which will notify about changes inside input fields. */ - fun addOnFieldStateChangeListener(fieldStateListener : OnFieldStateChangeListener?) { + fun addOnFieldStateChangeListener(fieldStateListener: OnFieldStateChangeListener?) { storage.attachStateChangeListener(fieldStateListener) } @@ -207,7 +216,7 @@ class VGSCollect { * Preferably call it inside onDestroy system's callback. */ fun onDestroy() { - currentTask?.cancel(true) + client.cancelAll() responseListeners.clear() storage.clear() } @@ -229,23 +238,15 @@ class VGSCollect { * @param path path for a request * @param method HTTP method */ - fun submit(path:String - , method:HTTPMethod = HTTPMethod.POST - ) { + fun submit(path: String, method: HTTPMethod = HTTPMethod.POST): VGSResponse { val request = VGSRequest.VGSRequestBuilder() .setPath(path) .setMethod(method) .build() - if(checkInternetPermission() && isUrlValid() && validateFields() && validateFiles()) { - doRequest(request) - submitEvent(true, !request.fileIgnore, !request.fieldsIgnore, - request.customHeader.isNotEmpty(), request.customData.isNotEmpty() - ) - } + return submit(request) } - /** * This method executes and send data on VGS Server. It could be useful if you want to handle * multithreading by yourself. @@ -253,19 +254,14 @@ class VGSCollect { * * @param request data class with attributes for submit. */ - fun submit(request: VGSRequest) { - if(isUrlValid() && checkInternetPermission()) { - if(!request.fieldsIgnore && !validateFields()) { - return - } - if(!request.fileIgnore&& !validateFiles()) { - return - } - doRequest(request) - submitEvent(true, !request.fileIgnore, !request.fieldsIgnore, - request.customHeader.isNotEmpty(), request.customData.isNotEmpty() - ) + fun submit(request: VGSRequest): VGSResponse { + var response: VGSResponse = VGSResponse.ErrorResponse() + + collectUserData(request) { + response = client.execute(request).toVGSResponse(context) } + + return response } /** @@ -274,21 +270,15 @@ class VGSCollect { * @param path path for a request * @param method HTTP method */ - fun asyncSubmit(path:String - , method:HTTPMethod + fun asyncSubmit( + path: String, method: HTTPMethod ) { val request = VGSRequest.VGSRequestBuilder() .setPath(path) .setMethod(method) .build() - if(checkInternetPermission() && isUrlValid() && validateFields() && validateFiles()) { - doAsyncRequest(request) - - submitEvent(true, !request.fileIgnore, !request.fieldsIgnore, - request.customHeader.isNotEmpty(), request.customData.isNotEmpty() - ) - } + asyncSubmit(request) } /** @@ -297,45 +287,48 @@ class VGSCollect { * @param request data class with attributes for submit */ fun asyncSubmit(request: VGSRequest) { - if(isUrlValid() && checkInternetPermission()) { - if(!request.fieldsIgnore && !validateFields()) { - return + collectUserData(request) { + client.enqueue(request) { r -> + mainHandler.post { notifyAllListeners(r.toVGSResponse()) } } - if(!request.fileIgnore && !validateFiles()) { - return - } - doAsyncRequest(request) - - submitEvent(true, !request.fileIgnore, !request.fieldsIgnore, - request.customHeader.isNotEmpty(), request.customData.isNotEmpty() - ) } } - private fun checkInternetPermission():Boolean { - return with(ContextCompat.checkSelfPermission(context, android.Manifest.permission.INTERNET) != PackageManager.PERMISSION_DENIED) { - if (this.not()) { - notifyErrorResponse(VGSError.NO_INTERNET_PERMISSIONS) + private fun collectUserData(request: VGSRequest, submitRequest: () -> Unit) { + when { + !request.fieldsIgnore && !validateFields() -> return + !request.fileIgnore && !validateFiles() -> return + !isURLValid -> notifyAllListeners(VGSError.URL_NOT_VALID.toVGSResponse(context)) + !context.hasInternetPermission() -> + notifyAllListeners(VGSError.NO_INTERNET_PERMISSIONS.toVGSResponse(context)) + !context.hasAccessNetworkStatePermission() -> + notifyAllListeners(VGSError.NO_NETWORK_CONNECTIONS.toVGSResponse(context)) + !context.isConnectionAvailable() -> + notifyAllListeners(VGSError.NO_NETWORK_CONNECTIONS.toVGSResponse(context)) + else -> { + mergeData(request.customData, request.fieldsIgnore, request.fileIgnore) + submitEvent( + true, + !request.fileIgnore, + !request.fieldsIgnore, + request.customHeader.isNotEmpty(), + request.customData.isNotEmpty() + ) + submitRequest() } - this } } - private fun isUrlValid():Boolean { - return with(isURLValid) { - if (this.not()) { - notifyErrorResponse(VGSError.URL_NOT_VALID) - } - this - } + private fun notifyAllListeners(r: VGSResponse) { + responseListeners.forEach { it.onResponse(r) } } - private fun validateFiles():Boolean { + private fun validateFiles(): Boolean { var isValid = true storage.getAttachedFiles().forEach { - if(it.size > storage.getFileSizeLimit()) { - notifyErrorResponse(VGSError.FILE_SIZE_OVER_LIMIT, it.name) + if (it.size > storage.getFileSizeLimit()) { + notifyAllListeners(VGSError.FILE_SIZE_OVER_LIMIT.toVGSResponse(context, it.name)) isValid = false return@forEach @@ -345,12 +338,17 @@ class VGSCollect { return isValid } - private fun validateFields():Boolean { + private fun validateFields(): Boolean { var isValid = true storage.getFieldsStorage().getItems().forEach { - if(it.isValid.not()) { - notifyErrorResponse(VGSError.INPUT_DATA_NOT_VALID, it.fieldName) + if (it.isValid.not()) { + VGSError.INPUT_DATA_NOT_VALID.toVGSResponse(context, it.fieldName).also { r -> + notifyAllListeners(r) + Logger.e(VGSCollect::class.java, r.localizeMessage) + submitEvent(false, code = r.errorCode) + } + isValid = false return isValid } @@ -358,51 +356,10 @@ class VGSCollect { return isValid } - private fun notifyErrorResponse(error: VGSError, vararg params:String?) { - val message = if(params.isEmpty()) { - context.getString(error.messageResId) - } else { - String.format( - context.getString(error.messageResId), - *params - ) - } - responseListeners.forEach { - it.onResponse(VGSResponse.ErrorResponse(message, error.code)) - } - Logger.e(VGSCollect::class.java, message) - submitEvent(false, code = error.code) - } - - private fun doRequest( - request: VGSRequest - ) { - val data = retrieveData(request.customData, request.fieldsIgnore, request.fileIgnore) - val r = client.call(request.path, request.method, request.customHeader, data) - responseListeners.forEach { it.onResponse(r) } - } - - private fun doAsyncRequest( - request: VGSRequest - ) { - if(currentTask?.isCancelled == false) { - currentTask?.cancel(true) - } - currentTask = doAsync(responseListeners) { - it?.run { - val data = retrieveData(request.customData, request.fieldsIgnore, request.fileIgnore) - val r = client.call(this.path, this.method, this.headers, data) - r - } ?: VGSResponse.ErrorResponse() - } - - val p = Payload(request.path, request.method, request.customHeader, request.customData) - currentTask!!.execute(p) - } - - private fun retrieveData(customData: HashMap = HashMap(), - fieldsIgnore:Boolean = false, - fileIgnore:Boolean = false + private fun mergeData( + customData: HashMap = HashMap(), + fieldsIgnore: Boolean = false, + fileIgnore: Boolean = false ): Map? { val requestBodyMap = customData.run { val map = HashMap() @@ -417,6 +374,7 @@ class VGSCollect { return storage.getAssociatedList(fieldsIgnore, fileIgnore) .mapUsefulPayloads(requestBodyMap) + ?.run { customData.deepMerge(this) } } /** @@ -436,12 +394,12 @@ class VGSCollect { fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { mapAnalyticEvent(data) - if(resultCode == Activity.RESULT_OK) { + if (resultCode == Activity.RESULT_OK) { val map: VGSHashMapWrapper? = data?.extras?.getParcelable( BaseTransmitActivity.RESULT_DATA ) - if(requestCode == TemporaryFileStorage.REQUEST_CODE) { + if (requestCode == TemporaryFileStorage.REQUEST_CODE) { map?.run { storage.getFileStorage().dispatch(mapOf()) } @@ -457,9 +415,9 @@ class VGSCollect { data?.let { val map: VGSHashMapWrapper = it.extras?.getParcelable( BaseTransmitActivity.RESULT_DATA - )?: VGSHashMapWrapper() + ) ?: VGSHashMapWrapper() - when(map.get(BaseTransmitActivity.RESULT_TYPE)) { + when (map.get(BaseTransmitActivity.RESULT_TYPE)) { BaseTransmitActivity.SCAN -> scanEvent( map.get(BaseTransmitActivity.RESULT_STATUS).toString(), map.get(BaseTransmitActivity.RESULT_NAME).toString(), @@ -533,29 +491,24 @@ class VGSCollect { client = c } - - - - - private fun initField(view: InputFieldView?) { val m = view?.getFieldType()?.getAnalyticName()?.run { with(mutableMapOf()) { put("field", this@run) this } - }?: mutableMapOf() + } ?: mutableMapOf() tracker.logEvent( InitAction(m) ) } - private fun scanEvent(status:String, type:String, id:String?) { + private fun scanEvent(status: String, type: String, id: String?) { val m = with(mutableMapOf()) { put("status", status) put("scannerType", type) - if(!id.isNullOrEmpty()) put("scanId", id.toString()) + if (!id.isNullOrEmpty()) put("scanId", id.toString()) this } @@ -564,36 +517,29 @@ class VGSCollect { ) } - private fun String.toMD5(): String { - val bytes = MessageDigest.getInstance("MD5").digest(this.toByteArray()) - return bytes.joinToString("") { "%02x".format(it) } - } - - private fun calculateCheckSum(): String { - return retrieveData()?.mapToJSON().toString().toMD5() - } - private fun submitEvent( isSuccess: Boolean, hasFiles: Boolean = false, hasFields: Boolean = false, hasCustomHeader: Boolean = false, hasCustomData: Boolean = false, - code:Int = 200 + code: Int = 200 ) { - if(code >= 1000 || code == 200) { + if (code.isHttpStatusCode()) { val m = with(mutableMapOf()) { if (isSuccess) put("status", "Ok") else put("status", "Failed") put("statusCode", code) - put("checkSum", calculateCheckSum()) - val arr = with(mutableListOf()) { if (hasFiles) add("file") if (hasFields) add("fields") - if (hasCustomHeader || client.getTemporaryStorage().getCustomHeaders().isNotEmpty()) add("customHeaders") - if (hasCustomData || client.getTemporaryStorage().getCustomData().isNotEmpty()) add("customData") + if (hasCustomHeader || + client.getTemporaryStorage().getCustomHeaders().isNotEmpty() + ) add("customHeaders") + if (hasCustomData || + client.getTemporaryStorage().getCustomData().isNotEmpty() + ) add("customData") this } @@ -608,13 +554,12 @@ class VGSCollect { } } - private fun responseEvent(code: Int, message:String? = null) { - if(code in 200..999) { + private fun responseEvent(code: Int, message: String? = null) { + if (code.isHttpStatusCode()) { val m = with(mutableMapOf()) { put("statusCode", code) put("status", BaseTransmitActivity.Status.SUCCESS.raw) - put("checkSum", calculateCheckSum()) - if(!message.isNullOrEmpty()) put("error", message) + if (!message.isNullOrEmpty()) put("error", message) this } @@ -624,7 +569,7 @@ class VGSCollect { } } - private fun attachFileEvent(status:String) {//MIME, success + private fun attachFileEvent(status: String) {//MIME, success val m = with(mutableMapOf()) { put("status", status) diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/AnalyticClient.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/AnalyticClient.kt deleted file mode 100644 index e37c14e1e..000000000 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/AnalyticClient.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.verygoodsecurity.vgscollect.core.api - -import android.content.Context -import android.net.ConnectivityManager -import android.util.Base64 -import okhttp3.OkHttpClient -import com.verygoodsecurity.vgscollect.core.HTTPMethod -import com.verygoodsecurity.vgscollect.core.VGSCollect -import com.verygoodsecurity.vgscollect.core.model.network.VGSError -import com.verygoodsecurity.vgscollect.core.model.network.VGSResponse -import com.verygoodsecurity.vgscollect.core.model.parseVGSResponse -import com.verygoodsecurity.vgscollect.util.Logger -import com.verygoodsecurity.vgscollect.util.mapToJSON -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import java.io.IOException -import java.io.InterruptedIOException -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException - -internal class AnalyticClient( - private val context: Context -) : ApiClient { - private var baseURL:String = "" - - private val client:OkHttpClient by lazy { - OkHttpClient().newBuilder() - .callTimeout(CONNECTION_TIME_OUT, TimeUnit.MILLISECONDS) - .readTimeout(CONNECTION_TIME_OUT, TimeUnit.MILLISECONDS) - .writeTimeout(CONNECTION_TIME_OUT, TimeUnit.MILLISECONDS) - .build() - } - - private fun hasNetworkAvailable(): Boolean { - val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? - val network = manager?.activeNetworkInfo - return (network != null) - } - - override fun call( - path: String, - method: HTTPMethod, - headers: Map?, - data: Map? - ): VGSResponse { - if(hasNetworkAvailable().not()) { - return notifyErrorResponse(VGSError.NO_NETWORK_CONNECTIONS) - } - - return when(method.ordinal) { - HTTPMethod.POST.ordinal -> postRequest(path, headers, data) - else -> VGSResponse.ErrorResponse() - } - } - - override fun getTemporaryStorage(): VgsApiTemporaryStorage { - return VgsApiTemporaryStorageImpl() - } - - private fun postRequest(path: String, headers: Map?, data: Map?): VGSResponse { - val url = baseURL.buildURL(path = path) - ?: return notifyErrorResponse(VGSError.URL_NOT_VALID) - - val requestBuilder = Request.Builder().url(url) - - addHeaders(requestBuilder, headers) - addRequestBody(requestBuilder, data) - - val request = requestBuilder.build() - - return try { - val response = client.newCall(request).execute() - - if (response.isSuccessful) { - val responseBodyStr = response.body?.string() - val responsePayload: Map? = responseBodyStr?.parseVGSResponse() - VGSResponse.SuccessResponse(responsePayload, responseBodyStr, response.code) - } else { - VGSResponse.ErrorResponse(response.message, response.code) - } - } catch (e: InterruptedIOException) { - notifyErrorResponse(VGSError.TIME_OUT) - } catch (e: TimeoutException) { - notifyErrorResponse(VGSError.TIME_OUT) - } catch (e: IOException) { - VGSResponse.ErrorResponse(e.message) - } - } - - private fun addRequestBody(requestBuilder: Request.Builder, data: Map?) { - val content = data?.mapToJSON().toString().toByteArray() - val bodyStr = Base64.encodeToString(content, Base64.NO_WRAP) - val body = bodyStr.toRequestBody(CONTENT_TYPE.toMediaTypeOrNull()) - requestBuilder.post(body) - } - - private fun addHeaders(requestBuilder: Request.Builder, headers: Map?) { - val storedHeaders = mutableMapOf() - headers?.let { storedHeaders.putAll(headers) } - storedHeaders.forEach { - requestBuilder.addHeader(it.key, it.value) - } - } - - private fun notifyErrorResponse(error: VGSError, vararg params:String?): VGSResponse.ErrorResponse { - val message = if(params.isEmpty()) { - context.getString(error.messageResId) - } else { - String.format( - context.getString(error.messageResId), - *params - ) - } - Logger.e(VGSCollect::class.java, message) - return VGSResponse.ErrorResponse(message, error.code) - } - - companion object { - private const val CONTENT_TYPE = "application/x-www-form-urlencoded" - private const val CONNECTION_TIME_OUT = 60000L - - fun newInstance(context: Context, baseURL:String):AnalyticClient { - val c = AnalyticClient(context) - c.baseURL = baseURL - return c - } - } -} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/AnalyticURLConnectionClient.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/AnalyticURLConnectionClient.kt deleted file mode 100644 index b11f24c6a..000000000 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/AnalyticURLConnectionClient.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.verygoodsecurity.vgscollect.core.api - -import com.verygoodsecurity.vgscollect.core.HTTPMethod -import com.verygoodsecurity.vgscollect.core.model.network.VGSResponse -import com.verygoodsecurity.vgscollect.core.model.parseVGSResponse -import com.verygoodsecurity.vgscollect.util.mapToJSON -import java.io.* -import java.net.HttpURLConnection -import java.net.HttpURLConnection.HTTP_OK -import java.net.URL -import java.nio.charset.Charset -import javax.net.ssl.HttpsURLConnection - -@Deprecated("from 1.1.0") -internal class AnalyticURLConnectionClient : ApiClient { - private var baseURL:String = "" - - private val tempStore:VgsApiTemporaryStorage by lazy { - VgsApiTemporaryStorageImpl() - } - - override fun call(path: String, method: HTTPMethod, headers: Map?, data: Map?): VGSResponse { - return when(method.ordinal) { - HTTPMethod.POST.ordinal -> postRequest(path, headers, data) - else -> VGSResponse.ErrorResponse() - } - } - - override fun getTemporaryStorage(): VgsApiTemporaryStorage = tempStore - - private fun postRequest(path: String, headers: Map? = null, data: Map? = null): VGSResponse { - val url = baseURL.buildURL(path = path) ?: return VGSResponse.ErrorResponse() - - var connection: HttpURLConnection? = null - var response: VGSResponse - try { - connection = openConnection(url) - - connection.requestMethod = HTTPMethod.POST.name - - addHeaders(connection, headers) - - writeOutput(connection, data) - - response = handleResponse(connection) - } catch (e: IOException) { - response = VGSResponse.ErrorResponse(e.localizedMessage) - } finally { - connection?.disconnect() - } - - return response - } - - private fun handleResponse(connection: HttpURLConnection): VGSResponse { - val responseCode = connection.responseCode - - return if (responseCode == HTTP_OK) { - val rawResponse = connection.inputStream?.bufferedReader()?.use { it.readText() } - val responsePayload:Map? = rawResponse?.parseVGSResponse() - VGSResponse.SuccessResponse(responsePayload, rawResponse, responseCode) - } else { - val responseStr = connection.errorStream?.bufferedReader()?.use { it.readText() } - VGSResponse.ErrorResponse(responseStr, responseCode) - } - } - - private fun writeOutput(connection: HttpURLConnection, data: Map?) { - val content = data?.mapToJSON().toString().toByteArray(Charset.forName(CHARSET)) - - val os: OutputStream = connection.outputStream - os.write(content) - os.close() - } - - private fun addHeaders(connection: HttpURLConnection, headers: Map?) { - connection.setRequestProperty( CONTENT_TYPE, CONTENT_TYPE_VALUE ) - headers?.forEach { - connection.setRequestProperty( it.key.toUpperCase(), it.value) - } - } - - private fun openConnection(url: URL): HttpURLConnection { - val connection = url.openConnection() as HttpURLConnection - if (connection is HttpsURLConnection) { - connection.sslSocketFactory = TLSSocketFactory() - } - - connection.useCaches = false - connection.allowUserInteraction = false - connection.connectTimeout = CONNECTION_TIME_OUT - connection.readTimeout = CONNECTION_TIME_OUT - connection.instanceFollowRedirects = false - - return connection - } - - companion object { - private const val CHARSET = "UTF-8" - - private const val CONNECTION_TIME_OUT = 30000 - - private const val CONTENT_TYPE = "Content-type" - private const val CONTENT_TYPE_VALUE = "application/x-www-form-urlencoded" - - fun newInstance(baseURL:String):ApiClient { - val client = AnalyticURLConnectionClient() - client.baseURL = baseURL - return client - } - } -} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/ApiClient.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/ApiClient.kt deleted file mode 100644 index 85fdbf7a5..000000000 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/ApiClient.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.verygoodsecurity.vgscollect.core.api - -import com.verygoodsecurity.vgscollect.core.HTTPMethod -import com.verygoodsecurity.vgscollect.core.model.network.VGSResponse - -internal interface ApiClient { - fun call( - path: String, - method: HTTPMethod, - headers: Map?, - data: Map? -): VGSResponse - - fun getTemporaryStorage():VgsApiTemporaryStorage -} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/OkHttpClient.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/OkHttpClient.kt deleted file mode 100644 index a2f4b16f5..000000000 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/OkHttpClient.kt +++ /dev/null @@ -1,187 +0,0 @@ -package com.verygoodsecurity.vgscollect.core.api - -import android.content.Context -import android.net.ConnectivityManager -import android.os.Build -import com.verygoodsecurity.vgscollect.BuildConfig -import com.verygoodsecurity.vgscollect.core.HTTPMethod -import com.verygoodsecurity.vgscollect.core.VGSCollect -import com.verygoodsecurity.vgscollect.core.api.analityc.CollectActionTracker -import com.verygoodsecurity.vgscollect.core.model.network.VGSError -import com.verygoodsecurity.vgscollect.core.model.network.VGSResponse -import com.verygoodsecurity.vgscollect.core.model.parseVGSResponse -import com.verygoodsecurity.vgscollect.util.Logger -import com.verygoodsecurity.vgscollect.util.mapToJSON -import okhttp3.Interceptor -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.Response -import java.io.IOException -import java.io.InterruptedIOException -import java.lang.StringBuilder -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException - -internal class OkHttpClient( - private val context: Context -):ApiClient { - private var baseURL:String = "" - - companion object { - private const val APPLICATION_JSON = "application/json; charset=utf-8" - private const val CONNECTION_TIME_OUT = 60000L - - private const val AGENT = "VGS-CLIENT" - private const val TEMPORARY_STR_AGENT = "source=androidSDK&medium=vgs-collect&content=${BuildConfig.VERSION_NAME}" - - fun newInstance(context: Context, baseURL:String):ApiClient { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val c = OkHttpClient(context) - c.baseURL = baseURL - c - } else { - URLConnectionClient.newInstance(context, baseURL) - } - } - } - - private val client:OkHttpClient by lazy { - OkHttpClient().newBuilder() - .addInterceptor(HttpLoggingInterceptor()) - .callTimeout(CONNECTION_TIME_OUT, TimeUnit.MILLISECONDS) - .readTimeout(CONNECTION_TIME_OUT, TimeUnit.MILLISECONDS) - .writeTimeout(CONNECTION_TIME_OUT, TimeUnit.MILLISECONDS) - .build() - } - - private val tempStore:VgsApiTemporaryStorage by lazy { - VgsApiTemporaryStorageImpl() - } - - override fun call( - path: String, - method: HTTPMethod, - headers: Map?, - data: Map? - ): VGSResponse { - if(hasNetworkAvailable().not()) { - return notifyErrorResponse(VGSError.NO_NETWORK_CONNECTIONS) - } - - return when(method.ordinal) { -// HTTPMethod.GET.ordinal -> getRequest(path, headers, data) - HTTPMethod.POST.ordinal -> postRequest(path, headers, data) - else -> VGSResponse.ErrorResponse() - } - } - - private fun postRequest( - path: String, - headers: Map?, - data: Map? - ): VGSResponse { - val url = baseURL.buildURL(path = path) - ?: return notifyErrorResponse(VGSError.URL_NOT_VALID) - val requestBuilder = Request.Builder().url(url) - - addHeaders(requestBuilder, headers) - addRequestBody(requestBuilder, data) - - val request = requestBuilder.build() - - return try { - val response = client.newCall(request).execute() - - if (response.isSuccessful) { - val responseBodyStr = response.body?.string() - val responsePayload: Map? = responseBodyStr?.parseVGSResponse() - VGSResponse.SuccessResponse(responsePayload, responseBodyStr, response.code) - } else { - VGSResponse.ErrorResponse(response.message, response.code) - } - } catch (e: InterruptedIOException) { - notifyErrorResponse(VGSError.TIME_OUT) - } catch (e: TimeoutException) { - notifyErrorResponse(VGSError.TIME_OUT) - } catch (e: IOException) { - VGSResponse.ErrorResponse(e.message) - } - } - - private fun addRequestBody(requestBuilder: Request.Builder, data: Map?) { - val content = data?.mapToJSON().toString() - val body = content.toRequestBody(APPLICATION_JSON.toMediaTypeOrNull()) - requestBuilder.post(body) - } - - private fun addHeaders(requestBuilder: Request.Builder, headers: Map?) { - val storedHeaders = tempStore.getCustomHeaders() - storedHeaders[AGENT] = TEMPORARY_STR_AGENT+"&vgsCollectSessionId=${CollectActionTracker.Sid.id}" - headers?.let { storedHeaders.putAll(headers) } - storedHeaders.forEach { - requestBuilder.addHeader(it.key, it.value) - } - } - - override fun getTemporaryStorage(): VgsApiTemporaryStorage = tempStore - - private fun hasNetworkAvailable(): Boolean { - val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? - val network = manager?.activeNetworkInfo - return (network != null) - } - - private fun notifyErrorResponse(error: VGSError, vararg params:String?): VGSResponse.ErrorResponse { - val message = if(params.isEmpty()) { - context.getString(error.messageResId) - } else { - String.format( - context.getString(error.messageResId), - *params - ) - } - Logger.e(VGSCollect::class.java, message) - return VGSResponse.ErrorResponse(message, error.code) - } - - - class HttpLoggingInterceptor: Interceptor { - - companion object { - private fun buildRequestLog(request: Request):String { - val builder = StringBuilder("Request") - .append("{") - .append("method=") - .append(request.method) - .append("}") - - return builder.toString() - } - - private fun buildResponseLog(response: Response):String { - val builder = StringBuilder("Response") - .append("{") - .append("code=") - .append(response.code.toString()) - .append(", ") - .append("message=") - .append(response.message) - .append("}") - - return builder.toString() - } - } - - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - Logger.i(VGSCollect::class.java.simpleName, buildRequestLog(request)) - - val response = chain.proceed(request) - Logger.i(VGSCollect::class.java.simpleName, buildResponseLog(response)) - - return response - } - } -} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/URLConnectionClient.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/URLConnectionClient.kt deleted file mode 100644 index 6bcebc0f8..000000000 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/URLConnectionClient.kt +++ /dev/null @@ -1,155 +0,0 @@ -package com.verygoodsecurity.vgscollect.core.api - -import android.content.Context -import com.verygoodsecurity.vgscollect.BuildConfig -import com.verygoodsecurity.vgscollect.core.HTTPMethod -import com.verygoodsecurity.vgscollect.core.VGSCollect -import com.verygoodsecurity.vgscollect.core.api.analityc.CollectActionTracker -import com.verygoodsecurity.vgscollect.core.model.network.VGSResponse -import com.verygoodsecurity.vgscollect.core.model.parseVGSResponse -import com.verygoodsecurity.vgscollect.util.Logger -import com.verygoodsecurity.vgscollect.util.mapToJSON -import java.io.* -import java.net.HttpURLConnection -import java.net.HttpURLConnection.HTTP_OK -import java.net.URL -import java.nio.charset.Charset -import javax.net.ssl.HttpsURLConnection - - -@Deprecated("from 1.1.0") -internal class URLConnectionClient( - private val context: Context -):ApiClient { - private var baseURL:String = "" - - private val tempStore:VgsApiTemporaryStorage by lazy { - VgsApiTemporaryStorageImpl() - } - - companion object { - private const val CHARSET = "UTF-8" - - private const val CONNECTION_TIME_OUT = 30000 - - private const val CONTENT_LENGTH = "Content-Length" - private const val CONTENT_TYPE = "Content-type" - private const val APPLICATION_JSON = "application/json" - - private const val AGENT = "vgs-client" - private const val TEMPORARY_STR_AGENT = "source=androidSDK&medium=vgs-collect&content=${BuildConfig.VERSION_NAME}" - - fun newInstance(context:Context, baseURL:String):ApiClient { - val client = URLConnectionClient(context) - client.baseURL = baseURL - return client - } - - private fun buildRequestLog(connection: HttpURLConnection):String { - val builder = StringBuilder("Request") - .append("{") - .append("method=") - .append(connection.requestMethod) - .append("}") - - return builder.toString() - } - - private fun buildResponseLog(connection: HttpURLConnection):String { - val builder = StringBuilder("Response") - .append("{") - .append("code=") - .append(connection.responseCode.toString()) - .append(", ") - .append("message=") - .append(connection.responseMessage) - .append("}") - - return builder.toString() - } - } - - override fun call(path: String, method: HTTPMethod, headers: Map?, data: Map?): VGSResponse { - return when(method.ordinal) { -// HTTPMethod.GET.ordinal -> getRequest(path, headers, data) - HTTPMethod.POST.ordinal -> postRequest(path, headers, data) - else -> VGSResponse.ErrorResponse() - } - } - - override fun getTemporaryStorage(): VgsApiTemporaryStorage = tempStore - - private fun postRequest(path: String, headers: Map? = null, data: Map? = null): VGSResponse { - val url = baseURL.buildURL(path = path) ?: return VGSResponse.ErrorResponse() - - var connection: HttpURLConnection? = null - var response: VGSResponse - try { - connection = openConnection(url) - - connection.requestMethod = HTTPMethod.POST.name - - addHeaders(connection, headers) - - writeOutput(connection, data) - - Logger.i(VGSCollect::class.java.simpleName, buildRequestLog(connection)) - - response = handleResponse(connection) - } catch (e: IOException) { - response = VGSResponse.ErrorResponse(e.localizedMessage) - Logger.e(VGSCollect::class.java, e.localizedMessage) - } finally { - connection?.disconnect() - } - - return response - } - - private fun handleResponse(connection: HttpURLConnection): VGSResponse { - val responseCode = connection.responseCode - - Logger.i(VGSCollect::class.java.simpleName, buildResponseLog(connection)) - - return if (responseCode == HTTP_OK) { - val rawResponse = connection.inputStream?.bufferedReader()?.use { it.readText() } - val responsePayload:Map? = rawResponse?.parseVGSResponse() - VGSResponse.SuccessResponse(responsePayload, rawResponse, responseCode) - } else { - val responseStr = connection.errorStream?.bufferedReader()?.use { it.readText() } - Logger.e(VGSCollect::class.java, responseStr.toString()) - VGSResponse.ErrorResponse(responseStr, responseCode) - } - } - - private fun writeOutput(connection: HttpURLConnection, data: Map?) { - val content = data?.mapToJSON().toString().toByteArray(Charset.forName(CHARSET)) - - val os: OutputStream = connection.outputStream - os.write(content) - os.close() - } - - private fun addHeaders(connection: HttpURLConnection, headers: Map?) { - connection.setRequestProperty( CONTENT_TYPE, APPLICATION_JSON ) - connection.setRequestProperty( AGENT, TEMPORARY_STR_AGENT+"&vgsCollectSessionId=${CollectActionTracker.Sid.id}" ) - headers?.forEach { - connection.setRequestProperty( it.key.toUpperCase(), it.value) - } - } - - private fun openConnection(url: URL): HttpURLConnection { - val connection = url.openConnection() as HttpURLConnection - if (connection is HttpsURLConnection) { - connection.sslSocketFactory = TLSSocketFactory() - } - - connection.useCaches = false - connection.allowUserInteraction = false - connection.connectTimeout = CONNECTION_TIME_OUT - connection.readTimeout = CONNECTION_TIME_OUT - connection.instanceFollowRedirects = false - - return connection - } -} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/UrlExtension.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/UrlExtension.kt index 8fe2c82fe..19eb7fdfc 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/UrlExtension.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/UrlExtension.kt @@ -53,31 +53,4 @@ internal fun String.isURLValid():Boolean { } catch (e:Exception) { false } -} - -internal fun String.buildURL(path: String, vararg getQuery:String):URL? { - val builder = StringBuilder(this) - - when { - path.isEmpty() -> {} - path.length > 1 && path.first().toString() == "/" -> builder.append(path) - else -> builder.append("/").append(path) - } - - if(getQuery.isNotEmpty()) { - builder.append("?") - getQuery.forEach { - if(builder.last() != '?') { - builder.append("&") - } - builder.append(it) - } - } - - var url:URL? = null - try { - url = URL(builder.toString()) - } catch (e: MalformedURLException) { } - - return url } \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/VGSHttpBodyFormat.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/VGSHttpBodyFormat.kt new file mode 100644 index 000000000..875957b9d --- /dev/null +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/VGSHttpBodyFormat.kt @@ -0,0 +1,9 @@ +package com.verygoodsecurity.vgscollect.core.api + +enum class VGSHttpBodyFormat { + JSON +} + +internal fun VGSHttpBodyFormat.toContentType() = when (this) { + VGSHttpBodyFormat.JSON -> "application/json" +} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/CollectActionTracker.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/CollectActionTracker.kt index 4dc7f43bf..fd41bf0a7 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/CollectActionTracker.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/CollectActionTracker.kt @@ -1,41 +1,26 @@ package com.verygoodsecurity.vgscollect.core.api.analityc -import android.content.Context import android.os.Build -import android.util.Log import com.verygoodsecurity.vgscollect.BuildConfig import com.verygoodsecurity.vgscollect.core.HTTPMethod -import com.verygoodsecurity.vgscollect.core.api.AnalyticClient -import com.verygoodsecurity.vgscollect.core.api.AnalyticURLConnectionClient -import com.verygoodsecurity.vgscollect.core.api.ApiClient +import com.verygoodsecurity.vgscollect.core.api.client.ApiClient import com.verygoodsecurity.vgscollect.core.api.analityc.action.Action -import com.verygoodsecurity.vgscollect.core.isLive +import com.verygoodsecurity.vgscollect.core.model.network.VGSRequest import java.util.* import java.util.concurrent.Executors internal class CollectActionTracker( - context: Context, val tnt: String, val environment: String, val formId: String ) : AnalyticTracker { internal object Sid { - val id = "${UUID.randomUUID()}" + val id = "${UUID.randomUUID()}" } - private val client:ApiClient by lazy { - val url = if(environment.isLive()) { - "https://vgs-collect-keeper.apps.verygood.systems" - } else { - "https://vgs-collect-keeper.verygoodsecurity.io" - } - - return@lazy if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - AnalyticClient.newInstance(context, url) - } else { - AnalyticURLConnectionClient.newInstance(url) - } + private val client: ApiClient by lazy { + return@lazy ApiClient.newHttpClient(URL) } override fun logEvent(action: Action) { @@ -49,19 +34,19 @@ internal class CollectActionTracker( } private class Event( - private val client:ApiClient, + private val client: ApiClient, private val tnt: String, private val environment: String, private val formId: String - ):Runnable { + ) : Runnable { - var map:MutableMap = mutableMapOf() + var map: MutableMap = mutableMapOf() set(value) { field = value field.putAll(attachDefaultInfo(value)) } - private fun attachDefaultInfo(map: MutableMap):Map { + private fun attachDefaultInfo(map: MutableMap): Map { return with(map) { this[VG_SESSION_ID] = Sid.id this[FORM_ID] = formId @@ -70,7 +55,7 @@ internal class CollectActionTracker( this[TNT] = tnt this[ENVIRONMENT] = environment this[VERSION] = BuildConfig.VERSION_NAME - if(!this.containsKey(STATUS)) { + if (!this.containsKey(STATUS)) { this[STATUS] = STATUS_OK } @@ -86,7 +71,13 @@ internal class CollectActionTracker( } override fun run() { - client.call(ENDPOINT, HTTPMethod.POST, null, map) + val r = VGSRequest.VGSRequestBuilder() + .setPath(ENDPOINT) + .setMethod(HTTPMethod.POST) + .setCustomData(map) + .build() + + client.enqueue(r) } companion object { @@ -111,6 +102,7 @@ internal class CollectActionTracker( } companion object { - private const val ENDPOINT = "/vgs" + private const val ENDPOINT = "/vgs" + private const val URL = "https://vgs-collect-keeper.apps.verygood.systems" } } \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/ApiClient.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/ApiClient.kt new file mode 100644 index 000000000..d676d809f --- /dev/null +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/ApiClient.kt @@ -0,0 +1,41 @@ +package com.verygoodsecurity.vgscollect.core.api.client + +import android.os.Build +import com.verygoodsecurity.vgscollect.core.api.VgsApiTemporaryStorage +import com.verygoodsecurity.vgscollect.core.model.network.NetworkResponse +import com.verygoodsecurity.vgscollect.core.model.network.VGSRequest +import com.verygoodsecurity.vgscollect.core.model.network.VGSResponse + +internal interface ApiClient { + + fun setURL(url: String) + + fun enqueue(request: VGSRequest, callback: ((NetworkResponse) -> Unit)? = null) + fun execute(request: VGSRequest): NetworkResponse + fun cancelAll() + + fun getTemporaryStorage(): VgsApiTemporaryStorage + + companion object { + internal const val CONNECTION_TIME_OUT = 60000L + internal const val AGENT = "vgs-client" + internal const val CONTENT_TYPE = "Content-type" + internal const val TEMPORARY_AGENT_TEMPLATE = "source=androidSDK&medium=vgs-collect&content=%s&vgsCollectSessionId=%s" + + fun newHttpClient(url: String): ApiClient { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + OkHttpClient().apply { setURL(url) } + } else { + URLConnectionClient.newInstance().apply { setURL(url) } + } + } + + fun newHttpClient(): ApiClient { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + OkHttpClient() + } else { + URLConnectionClient.newInstance() + } + } + } +} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/OkHttpClient.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/OkHttpClient.kt new file mode 100644 index 000000000..ee5f9ecfb --- /dev/null +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/OkHttpClient.kt @@ -0,0 +1,212 @@ +package com.verygoodsecurity.vgscollect.core.api.client + +import com.verygoodsecurity.vgscollect.BuildConfig +import com.verygoodsecurity.vgscollect.core.HTTPMethod +import com.verygoodsecurity.vgscollect.core.VGSCollect +import com.verygoodsecurity.vgscollect.core.api.* +import com.verygoodsecurity.vgscollect.core.api.client.ApiClient.Companion.AGENT +import com.verygoodsecurity.vgscollect.core.api.client.ApiClient.Companion.CONNECTION_TIME_OUT +import com.verygoodsecurity.vgscollect.core.api.client.ApiClient.Companion.TEMPORARY_AGENT_TEMPLATE +import com.verygoodsecurity.vgscollect.core.api.VgsApiTemporaryStorage +import com.verygoodsecurity.vgscollect.core.api.VgsApiTemporaryStorageImpl +import com.verygoodsecurity.vgscollect.core.api.analityc.CollectActionTracker +import com.verygoodsecurity.vgscollect.core.api.client.extension.isCodeSuccessful +import com.verygoodsecurity.vgscollect.core.api.client.extension.setMethod +import com.verygoodsecurity.vgscollect.core.model.network.NetworkResponse +import com.verygoodsecurity.vgscollect.core.model.network.VGSError +import com.verygoodsecurity.vgscollect.core.model.network.VGSRequest +import com.verygoodsecurity.vgscollect.util.Logger +import com.verygoodsecurity.vgscollect.util.extension.concatWithSlash +import com.verygoodsecurity.vgscollect.util.mapToJSON +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okio.Buffer +import java.io.IOException +import java.io.InterruptedIOException +import java.lang.StringBuilder +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +internal class OkHttpClient : ApiClient { + + private val client: OkHttpClient by lazy { + OkHttpClient().newBuilder() + .addInterceptor(HttpLoggingInterceptor()) + .callTimeout(CONNECTION_TIME_OUT, TimeUnit.MILLISECONDS) + .readTimeout(CONNECTION_TIME_OUT, TimeUnit.MILLISECONDS) + .writeTimeout(CONNECTION_TIME_OUT, TimeUnit.MILLISECONDS) + .build() + } + + private val tempStore: VgsApiTemporaryStorage by lazy { + VgsApiTemporaryStorageImpl() + } + + private var baseUrl: String = "" + override fun setURL(url: String) { + baseUrl = url + } + + private fun printBody(okHttpRequest: Request) { //todo remove after implementation CNAme + try { + val copy: RequestBody = okHttpRequest.body!! + val buffer = Buffer() + copy.writeTo(buffer) + + Logger.i(OkHttpClient::class.java.canonicalName, "${buffer.readUtf8()}") + } catch (e: IOException) { + Logger.i(OkHttpClient::class.java.canonicalName, "no body") + } + } + + override fun enqueue(request: VGSRequest, callback: ((NetworkResponse) -> Unit)?) { + val url = (baseUrl concatWithSlash request.path) + + if (!url.isURLValid()) { + callback?.invoke(NetworkResponse(error = VGSError.URL_NOT_VALID)) + return + } + + val okHttpRequest = buildRequest( + url, + request.method, + request.customHeader, + request.customData, + request.format + ) + + printBody(okHttpRequest) + + try { + client.newCall(okHttpRequest).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + callback?.invoke(NetworkResponse(message = e.message)) + } + + override fun onResponse(call: Call, response: Response) { + callback?.invoke( + NetworkResponse( + response.code.isCodeSuccessful(), + response.body?.string(), + response.code + ) + ) + } + }) + } catch (e: InterruptedIOException) { + callback?.invoke(NetworkResponse(error = VGSError.TIME_OUT)) + } catch (e: TimeoutException) { + callback?.invoke(NetworkResponse(error = VGSError.TIME_OUT)) + } catch (e: IOException) { + callback?.invoke(NetworkResponse(message = e.message)) + } + } + + override fun execute(request: VGSRequest): NetworkResponse { + val url = (baseUrl concatWithSlash request.path) + + if (!url.isURLValid()) { + return NetworkResponse(error = VGSError.URL_NOT_VALID) + } + + val okHttpRequest = buildRequest( + url, + request.method, + request.customHeader, + request.customData, + request.format + ) + + return try { + val response = client.newCall(okHttpRequest).execute() + + if (response.isSuccessful) { + NetworkResponse(response.isSuccessful, response.body?.string(), response.code) + } else { + NetworkResponse(message = response.message, code = response.code) + } + } catch (e: InterruptedIOException) { + NetworkResponse(error = VGSError.TIME_OUT) + } catch (e: TimeoutException) { + NetworkResponse(error = VGSError.TIME_OUT) + } catch (e: IOException) { + NetworkResponse(message = e.message) + } + } + + override fun getTemporaryStorage(): VgsApiTemporaryStorage = tempStore + + private fun buildRequest( + url: String, + method: HTTPMethod, + headers: Map?, + data: Map?, + contentType: VGSHttpBodyFormat = VGSHttpBodyFormat.JSON + ): Request { + return Request.Builder().url(url).setMethod( + method, + data?.mapToJSON().toString(), + contentType.toContentType().toMediaTypeOrNull() + ) + .addHeaders(headers) + .build() + } + + private fun Request.Builder.addHeaders(headers: Map?): Request.Builder { + val storedHeaders = tempStore.getCustomHeaders() + storedHeaders[AGENT] = String.format( + TEMPORARY_AGENT_TEMPLATE, + BuildConfig.VERSION_NAME, + CollectActionTracker.Sid.id + ) + headers?.let { storedHeaders.putAll(headers) } + storedHeaders.forEach { + this.addHeader(it.key, it.value) + } + + return this + } + + class HttpLoggingInterceptor : Interceptor { + + companion object { + private fun buildRequestLog(request: Request): String { + val builder = StringBuilder("Request") + .append("{") + .append("method=") + .append(request.method) + .append("}") + + return builder.toString() + } + + private fun buildResponseLog(response: Response): String { + val builder = StringBuilder("Response") + .append("{") + .append("code=") + .append(response.code.toString()) + .append(", ") + .append("message=") + .append(response.message) + .append("}") + + return builder.toString() + } + } + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + Logger.i(VGSCollect::class.java.simpleName, buildRequestLog(request)) + + val response = chain.proceed(request) + Logger.i(VGSCollect::class.java.simpleName, buildResponseLog(response)) + + return response + } + } + + override fun cancelAll() { + client.dispatcher.cancelAll() + } +} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/URLConnectionClient.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/URLConnectionClient.kt new file mode 100644 index 000000000..c1feb0383 --- /dev/null +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/URLConnectionClient.kt @@ -0,0 +1,150 @@ +package com.verygoodsecurity.vgscollect.core.api.client + +import com.verygoodsecurity.vgscollect.BuildConfig +import com.verygoodsecurity.vgscollect.core.VGSCollect +import com.verygoodsecurity.vgscollect.core.api.* +import com.verygoodsecurity.vgscollect.core.api.client.ApiClient.Companion.AGENT +import com.verygoodsecurity.vgscollect.core.api.client.ApiClient.Companion.CONNECTION_TIME_OUT +import com.verygoodsecurity.vgscollect.core.api.client.ApiClient.Companion.CONTENT_TYPE +import com.verygoodsecurity.vgscollect.core.api.client.ssl.TLSSocketFactory +import com.verygoodsecurity.vgscollect.core.api.VgsApiTemporaryStorage +import com.verygoodsecurity.vgscollect.core.api.VgsApiTemporaryStorageImpl +import com.verygoodsecurity.vgscollect.core.api.analityc.CollectActionTracker +import com.verygoodsecurity.vgscollect.core.api.client.extension.* +import com.verygoodsecurity.vgscollect.core.api.client.extension.callTimeout +import com.verygoodsecurity.vgscollect.core.api.client.extension.openConnection +import com.verygoodsecurity.vgscollect.core.api.client.extension.readTimeout +import com.verygoodsecurity.vgscollect.core.api.client.extension.setSSLSocketFactory +import com.verygoodsecurity.vgscollect.core.model.network.NetworkResponse +import com.verygoodsecurity.vgscollect.core.model.network.VGSError +import com.verygoodsecurity.vgscollect.core.model.network.VGSRequest +import com.verygoodsecurity.vgscollect.util.Logger +import com.verygoodsecurity.vgscollect.util.extension.concatWithSlash +import com.verygoodsecurity.vgscollect.util.mapToJSON +import java.net.HttpURLConnection +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.Future + +internal class URLConnectionClient : ApiClient { + private val submittedTasks = mutableListOf>() + private val executor: ExecutorService by lazy { + Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) + } + + private var baseURL:String = "" + + private val tempStore: VgsApiTemporaryStorage by lazy { + VgsApiTemporaryStorageImpl() + } + + override fun setURL(url: String) { + baseURL = url + } + + override fun enqueue(request: VGSRequest, callback: ((NetworkResponse) -> Unit)?) { + var task: Future<*>? = null + task = executor.submit { + try { + callback?.invoke(this.execute(request)) + submittedTasks.remove(task) + } catch (e: Exception) { + callback?.invoke(NetworkResponse(error = VGSError.TIME_OUT)) + } + } + submittedTasks.add(task) + } + + override fun execute(request: VGSRequest): NetworkResponse { + val url = (baseURL concatWithSlash request.path) + if(!url.isURLValid()) { + return NetworkResponse(error = VGSError.URL_NOT_VALID) + } + + var connection: HttpURLConnection? = null + return try { + connection = url.openConnection() + .setSSLSocketFactory(TLSSocketFactory()) + .callTimeout(CONNECTION_TIME_OUT) + .readTimeout(CONNECTION_TIME_OUT) + .setInstanceFollowRedirectEnabled(false) + .setIsUserInteractionEnabled(false) + .setCacheEnabled(false) + .addHeader(CONTENT_TYPE, request.format.toContentType()) + .addHeader(AGENT, String.format(ApiClient.TEMPORARY_AGENT_TEMPLATE, BuildConfig.VERSION_NAME, CollectActionTracker.Sid.id)) + .addHeaders(request.customHeader) + .setMethod(request.method) + + Logger.i(VGSCollect::class.java.simpleName, buildRequestLog(connection)) + writeOutput(connection, request.customData) + + handleResponse(connection) + } catch (e: Exception) { + Logger.e(VGSCollect::class.java, e.localizedMessage?:"") + NetworkResponse(message = e.localizedMessage) + } finally { + connection?.disconnect() + } + } + + override fun cancelAll() { + submittedTasks.forEach { + it.cancel(true) + } + submittedTasks.clear() + } + + override fun getTemporaryStorage(): VgsApiTemporaryStorage = tempStore + + private fun handleResponse(connection: HttpURLConnection): NetworkResponse { + val responseCode = connection.responseCode + + Logger.i(VGSCollect::class.java.simpleName, buildResponseLog(connection)) + + return if (responseCode.isCodeSuccessful()) { + val rawResponse = connection.inputStream?.bufferedReader()?.use { it.readText() } + NetworkResponse(true, rawResponse, responseCode) + } else { + val responseStr = connection.errorStream?.bufferedReader()?.use { it.readText() } + Logger.e(VGSCollect::class.java, responseStr.toString()) + NetworkResponse(message = responseStr, code = responseCode) + } + } + + private fun writeOutput(connection: HttpURLConnection, data: Map?) { + data?.mapToJSON().toString().toByteArray(Charsets.UTF_8).let { + connection.outputStream.use { os -> + os.write(it) + } + } + } + + companion object { + fun newInstance(): ApiClient { + return URLConnectionClient() + } + + private fun buildRequestLog(connection: HttpURLConnection):String { + val builder = StringBuilder("Request") + .append("{") + .append("method=") + .append(connection.requestMethod) + .append("}") + + return builder.toString() + } + + private fun buildResponseLog(connection: HttpURLConnection):String { + val builder = StringBuilder("Response") + .append("{") + .append("code=") + .append(connection.responseCode.toString()) + .append(", ") + .append("message=") + .append(connection.responseMessage) + .append("}") + + return builder.toString() + } + } +} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/extension/HttpUrlConnection.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/extension/HttpUrlConnection.kt new file mode 100644 index 000000000..8369603db --- /dev/null +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/extension/HttpUrlConnection.kt @@ -0,0 +1,65 @@ +package com.verygoodsecurity.vgscollect.core.api.client.extension + +import com.verygoodsecurity.vgscollect.core.HTTPMethod +import java.net.HttpURLConnection +import java.net.MalformedURLException +import java.net.URL +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLSocketFactory + + +@Throws(ClassCastException::class, MalformedURLException::class) +internal fun String.openConnection(): HttpURLConnection { + return (URL(this).openConnection() as HttpURLConnection).apply { + useCaches = false + allowUserInteraction = false + instanceFollowRedirects = false + } +} + +internal fun HttpURLConnection.callTimeout(timeout: Long): HttpURLConnection { + connectTimeout = timeout.toInt() + return this +} + +internal fun HttpURLConnection.readTimeout(timeout: Long): HttpURLConnection { + readTimeout = timeout.toInt() + return this +} + +internal fun HttpURLConnection.setSSLSocketFactory(factory: SSLSocketFactory): HttpURLConnection { + (this as? HttpsURLConnection)?.sslSocketFactory = factory + return this +} + +internal fun HttpURLConnection.setCacheEnabled(enabled: Boolean): HttpURLConnection { + useCaches = enabled + return this +} + +internal fun HttpURLConnection.setIsUserInteractionEnabled(enabled: Boolean): HttpURLConnection { + allowUserInteraction = enabled + return this +} + +internal fun HttpURLConnection.setInstanceFollowRedirectEnabled(enabled: Boolean): HttpURLConnection { + instanceFollowRedirects = enabled + return this +} + +internal fun HttpURLConnection.addHeader(key: String, value: String): HttpURLConnection { + setRequestProperty(key, value) + return this +} + +internal fun HttpURLConnection.addHeaders(headers: Map?): HttpURLConnection { + headers?.forEach { + setRequestProperty(it.key, it.value) + } + return this +} + +internal fun HttpURLConnection.setMethod(method: HTTPMethod): HttpURLConnection { + requestMethod = method.name + return this +} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/extension/OkHttpConnection.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/extension/OkHttpConnection.kt new file mode 100644 index 000000000..ee4bf8163 --- /dev/null +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/extension/OkHttpConnection.kt @@ -0,0 +1,19 @@ +package com.verygoodsecurity.vgscollect.core.api.client.extension + +import com.verygoodsecurity.vgscollect.core.HTTPMethod +import okhttp3.MediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.internal.EMPTY_REQUEST + +fun Request.Builder.setMethod( + method: HTTPMethod, + data: String?, + mediaType: MediaType? +): Request.Builder { + return if (method == HTTPMethod.POST) { + post(data?.toRequestBody(mediaType) ?: EMPTY_REQUEST) + } else { + return this + } +} diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/extension/Response.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/extension/Response.kt new file mode 100644 index 000000000..997595f80 --- /dev/null +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/extension/Response.kt @@ -0,0 +1,9 @@ +package com.verygoodsecurity.vgscollect.core.api.client.extension + +fun Int.isCodeSuccessful(): Boolean { + return this in 200..299 +} + +fun Int.isHttpStatusCode(): Boolean { + return this in 200..999 +} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/TLSSocketFactory.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/ssl/TLSSocketFactory.kt similarity index 98% rename from vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/TLSSocketFactory.kt rename to vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/ssl/TLSSocketFactory.kt index c6f80a892..0aaa8b0a9 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/TLSSocketFactory.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/ssl/TLSSocketFactory.kt @@ -1,4 +1,4 @@ -package com.verygoodsecurity.vgscollect.core.api +package com.verygoodsecurity.vgscollect.core.api.client.ssl import androidx.annotation.VisibleForTesting import java.io.IOException diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/doAsync.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/doAsync.kt deleted file mode 100644 index 5c9a1ae9d..000000000 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/doAsync.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.verygoodsecurity.vgscollect.core.api - -import android.os.AsyncTask -import com.verygoodsecurity.vgscollect.core.VgsCollectResponseListener -import com.verygoodsecurity.vgscollect.core.model.Payload -import com.verygoodsecurity.vgscollect.core.model.network.VGSResponse -import java.lang.ref.WeakReference - -internal class doAsync(listeners: MutableList, val handler: (arg: Payload?) -> VGSResponse) : AsyncTask() { - var onResponseListeners: WeakReference>? = WeakReference(listeners) - override fun doInBackground(vararg arg: Payload?): VGSResponse? { - val param = if(!arg.isNullOrEmpty()) { - arg[0] - } else { - null - } - - return handler(param) - } - - override fun onPostExecute(result: VGSResponse?) { - super.onPostExecute(result) - onResponseListeners?.get()?.forEach { - it.onResponse(result) - } - } - - override fun onCancelled(result: VGSResponse?) { - onResponseListeners?.clear() - super.onCancelled(result) - } -} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/NetworkResponse.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/NetworkResponse.kt new file mode 100644 index 000000000..5ee5fa782 --- /dev/null +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/NetworkResponse.kt @@ -0,0 +1,30 @@ +package com.verygoodsecurity.vgscollect.core.model.network + +import android.content.Context + +data class NetworkResponse( + val isSuccessful: Boolean = false, + val body: String? = null, + val code: Int = -1, + val message: String? = null, + val error: VGSError? = null +) + +fun NetworkResponse.toVGSResponse( + context: Context? = null +): VGSResponse { + return when { + this.isSuccessful -> VGSResponse.SuccessResponse( + rawResponse = this.body, + successCode = this.code + ) + this.error != null -> VGSResponse.ErrorResponse( + localizeMessage = context?.getString(error.messageResId) ?: "", + errorCode = error.code + ) + else -> VGSResponse.ErrorResponse( + localizeMessage = this.message ?: "", + errorCode = this.code + ) + } +} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSError.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSError.kt index 3a58b6420..91bb3d44e 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSError.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSError.kt @@ -1,5 +1,6 @@ package com.verygoodsecurity.vgscollect.core.model.network +import android.content.Context import com.verygoodsecurity.vgscollect.R enum class VGSError(val code:Int, val messageResId:Int) { @@ -30,4 +31,14 @@ enum class VGSError(val code:Int, val messageResId:Int) { FILE_SIZE_OVER_LIMIT(1103, R.string.error_file_size_validation ) +} + +fun VGSError.toVGSResponse(context: Context, vararg params: String?): VGSResponse.ErrorResponse { + val message = if (params.isEmpty()) { + context.getString(this.messageResId) + } else { + String.format(context.getString(this.messageResId), *params) + } + + return VGSResponse.ErrorResponse(localizeMessage = message, errorCode = this.code) } \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSRequest.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSRequest.kt index 6311219e1..04c8e27a7 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSRequest.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSRequest.kt @@ -1,6 +1,7 @@ package com.verygoodsecurity.vgscollect.core.model.network import com.verygoodsecurity.vgscollect.core.HTTPMethod +import com.verygoodsecurity.vgscollect.core.api.VGSHttpBodyFormat /** * Class to collect data before submit. @@ -16,11 +17,12 @@ import com.verygoodsecurity.vgscollect.core.HTTPMethod */ data class VGSRequest private constructor( val method: HTTPMethod, - val path:String, - val customHeader:HashMap, - val customData:HashMap, - val fieldsIgnore:Boolean = false, - val fileIgnore:Boolean = false + val path: String, + val customHeader: HashMap, + val customData: HashMap, + val fieldsIgnore: Boolean = false, + val fileIgnore: Boolean = false, + val format: VGSHttpBodyFormat = VGSHttpBodyFormat.JSON ) { /** @@ -29,11 +31,12 @@ data class VGSRequest private constructor( */ class VGSRequestBuilder { private var method: HTTPMethod = HTTPMethod.POST - private var path:String = "/post" - private val customHeader:HashMap = HashMap() - private val customData:HashMap = HashMap() - private var fieldsIgnore:Boolean = false - private var fileIgnore:Boolean = false + private var path: String = "/post" + private val customHeader: HashMap = HashMap() + private val customData: HashMap = HashMap() + private var fieldsIgnore: Boolean = false + private var format: VGSHttpBodyFormat = VGSHttpBodyFormat.JSON + private var fileIgnore: Boolean = false /** * It collect custom data which will be send to the server. @@ -41,7 +44,7 @@ data class VGSRequest private constructor( * @param customData The Map to save for request. * @return current builder instance */ - fun setCustomData(customData:Map): VGSRequestBuilder { + fun setCustomData(customData: Map): VGSRequestBuilder { this.customData.putAll(customData) return this } @@ -52,7 +55,7 @@ data class VGSRequest private constructor( * @param customHeader The headers to save for request. * @return current builder instance */ - fun setCustomHeader(customHeader:Map): VGSRequestBuilder { + fun setCustomHeader(customHeader: Map): VGSRequestBuilder { this.customHeader.putAll(customHeader) return this } @@ -63,12 +66,12 @@ data class VGSRequest private constructor( * @param path path for a request * @return current builder instance */ - fun setPath(path:String): VGSRequestBuilder { + fun setPath(path: String): VGSRequestBuilder { this.path = path.run { val p = when { length == 0 -> "/" first() == '/' -> this - else -> "/$this" + else -> "/$this" } p } @@ -99,6 +102,10 @@ data class VGSRequest private constructor( return this } + internal fun setFormat(format: VGSHttpBodyFormat): VGSRequestBuilder { + return this.apply { this.format = format } + } + /** * Ignore files in a request to the server. * @@ -106,7 +113,7 @@ data class VGSRequest private constructor( * @since 1.0.10 */ fun ignoreFiles(): VGSRequestBuilder { - fieldsIgnore = true + fileIgnore = true return this } diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSResponse.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSResponse.kt index 0eb306192..f8361259d 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSResponse.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSResponse.kt @@ -7,7 +7,7 @@ package com.verygoodsecurity.vgscollect.core.model.network * * @version 1.0.1 */ -sealed class VGSResponse(val code:Int = -1) { +sealed class VGSResponse(val code: Int = -1) { /** * The class definition for a success response state. @@ -17,10 +17,11 @@ sealed class VGSResponse(val code:Int = -1) { * @param successCode The response code from server. */ class SuccessResponse( - val response:Map? = null, - val rawResponse:String? = null, - val successCode:Int = -1 - ): VGSResponse(successCode) { + @Deprecated("rawResponse attribute better to use for response parsing") + val response: Map? = null, + val rawResponse: String? = null, + val successCode: Int = -1 + ) : VGSResponse(successCode) { override fun toString(): String { return "Code: $successCode \n $rawResponse" } @@ -33,12 +34,11 @@ sealed class VGSResponse(val code:Int = -1) { * @param errorCode The response code from server. */ class ErrorResponse( - val localizeMessage:String? = "Can't connect to server", - val errorCode:Int = -1 - ): VGSResponse(errorCode) { + val localizeMessage: String = "Can't connect to server", + val errorCode: Int = -1, + ) : VGSResponse(errorCode) { override fun toString(): String { return "Code: $errorCode \n $localizeMessage" } } - } \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/state/FieldContent.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/state/FieldContent.kt index 21fb6ba8e..110f49ac5 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/state/FieldContent.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/state/FieldContent.kt @@ -1,6 +1,6 @@ package com.verygoodsecurity.vgscollect.core.model.state -import com.verygoodsecurity.vgscollect.util.isNumeric +import com.verygoodsecurity.vgscollect.util.extension.isNumeric import com.verygoodsecurity.vgscollect.view.card.CardType import java.lang.StringBuilder import java.text.SimpleDateFormat diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt index 2b459a50b..518a4654d 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt @@ -9,7 +9,7 @@ import androidx.annotation.VisibleForTesting import com.verygoodsecurity.vgscollect.app.FilePickerActivity import com.verygoodsecurity.vgscollect.core.model.network.VGSError import com.verygoodsecurity.vgscollect.core.model.state.FileState -import com.verygoodsecurity.vgscollect.util.parseFile +import com.verygoodsecurity.vgscollect.util.extension.parseFile import java.util.HashMap internal class TemporaryFileStorage( diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/BooleanExtension.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/BooleanExtension.kt deleted file mode 100644 index 9e11c26dd..000000000 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/BooleanExtension.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.verygoodsecurity.vgscollect.util - -/** @suppress */ -internal fun Boolean.toInt():Int { - return if(this) { - 1 - } else { - 0 - } -} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/CommonUtil.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/CommonUtil.kt deleted file mode 100644 index f58c86a47..000000000 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/CommonUtil.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.verygoodsecurity.vgscollect.util - -import java.io.File - -/** @suppress */ -internal object CommonUtil { - fun isRooted():Boolean { - val buildTags = android.os.Build.TAGS - if (buildTags != null && buildTags.contains("test-keys")) { - return true - } - - // check if /system/app/Superuser.apk is present - try { - val file = File("/system/app/Superuser.apk") - if (file.exists()) { - return true - } - } catch (e1: Exception) { - // ignore - } - - return canExecuteCommand("su") - } - - private fun canExecuteCommand(command:String):Boolean { - return try { - Runtime.getRuntime().exec(command) - true - } catch (e: Exception) { - false - } - } -} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/extension/NetworkConnection.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/extension/NetworkConnection.kt new file mode 100644 index 000000000..8463b527a --- /dev/null +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/extension/NetworkConnection.kt @@ -0,0 +1,26 @@ +package com.verygoodsecurity.vgscollect.util.extension + +import android.content.Context +import android.content.pm.PackageManager +import android.net.ConnectivityManager +import androidx.core.content.ContextCompat + +fun Context.isConnectionAvailable(): Boolean { + return if (hasAccessNetworkStatePermission()) { + val manager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? + val network = manager?.activeNetworkInfo + (network != null) + } else { + false + } +} + +fun Context.hasAccessNetworkStatePermission() : Boolean = ContextCompat.checkSelfPermission( + this, + android.Manifest.permission.ACCESS_NETWORK_STATE +) != PackageManager.PERMISSION_DENIED + +fun Context.hasInternetPermission() = ContextCompat.checkSelfPermission( + this, + android.Manifest.permission.INTERNET +) != PackageManager.PERMISSION_DENIED diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/NumberExtension.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/extension/Number.kt similarity index 64% rename from vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/NumberExtension.kt rename to vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/extension/Number.kt index 56fda2cde..2510807ba 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/NumberExtension.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/extension/Number.kt @@ -1,4 +1,4 @@ -package com.verygoodsecurity.vgscollect.util +package com.verygoodsecurity.vgscollect.util.extension /** @suppress */ internal fun String.isNumeric():Boolean { diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/extension/String.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/extension/String.kt new file mode 100644 index 000000000..60741c593 --- /dev/null +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/extension/String.kt @@ -0,0 +1,16 @@ +package com.verygoodsecurity.vgscollect.util.extension + + +internal infix fun String.concatWithDash(suffix: String): String { + return when { + suffix.isEmpty() -> this + suffix.startsWith('-') -> this + suffix + else -> "$this-$suffix" + } +} + +internal infix fun String.concatWithSlash(suffix: String): String = when { + suffix.isEmpty() -> this + suffix.startsWith("/") -> this + suffix + else -> "$this/$suffix" +} diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/UriExtension.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/extension/Uri.kt similarity index 79% rename from vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/UriExtension.kt rename to vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/extension/Uri.kt index e3362a249..d15add7be 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/UriExtension.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/extension/Uri.kt @@ -1,4 +1,4 @@ -package com.verygoodsecurity.vgscollect.util +package com.verygoodsecurity.vgscollect.util.extension import android.content.Context import android.net.Uri @@ -28,12 +28,4 @@ internal fun Uri.parseFile(context: Context, fieldName: String): FileState? { mimeType, fieldName ) -} - -internal infix fun String.with(suffix: String): String { - return when { - suffix.isEmpty() -> this - suffix[0] == '-' -> this + suffix - else -> "$this-$suffix" - } } \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/internal/CardInputField.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/internal/CardInputField.kt index df026e3f5..6c53faa91 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/internal/CardInputField.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/internal/CardInputField.kt @@ -11,7 +11,7 @@ import android.view.View import com.verygoodsecurity.vgscollect.R import com.verygoodsecurity.vgscollect.core.model.state.* import com.verygoodsecurity.vgscollect.util.Logger -import com.verygoodsecurity.vgscollect.util.isNumeric +import com.verygoodsecurity.vgscollect.util.extension.isNumeric import com.verygoodsecurity.vgscollect.view.InputFieldView import com.verygoodsecurity.vgscollect.view.card.* import com.verygoodsecurity.vgscollect.view.card.conection.InputCardNumberConnection diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/NumberExtensionTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/NumberExtensionTest.kt index 2b0132ae7..d8b70d0e9 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/NumberExtensionTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/NumberExtensionTest.kt @@ -1,6 +1,6 @@ package com.verygoodsecurity.vgscollect -import com.verygoodsecurity.vgscollect.util.isNumeric +import com.verygoodsecurity.vgscollect.util.extension.isNumeric import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/TestApplication.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/TestApplication.kt index 309d796f0..93d3d9fde 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/TestApplication.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/TestApplication.kt @@ -2,11 +2,13 @@ package com.verygoodsecurity.vgscollect import android.app.Application import com.verygoodsecurity.vgscollect.R +import org.mockito.Mockito internal class TestApplication: Application() { override fun onCreate() { super.onCreate() setTheme(R.style.AppTheme) } +} -} \ No newline at end of file +fun any(): T = Mockito.any() diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/UrlExtensionTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/UrlExtensionTest.kt index 3d7c7f2ec..e995dbea9 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/UrlExtensionTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/UrlExtensionTest.kt @@ -2,7 +2,6 @@ package com.verygoodsecurity.vgscollect import com.verygoodsecurity.vgscollect.core.Environment import com.verygoodsecurity.vgscollect.core.api.* -import com.verygoodsecurity.vgscollect.util.with import org.junit.Assert.* import org.junit.Test import java.util.regex.Pattern @@ -12,42 +11,6 @@ class UrlExtensionTest { private const val URL_REGEX = "^(https:\\/\\/)+[a-zA-Z0-9]+[.]+((live|sandbox|LIVE|SANDBOX)+((-)+([a-zA-Z0-9]+)|)+)+[.](verygoodproxy.com)\$" } - @Test - fun test_build_URL_fault() { - val url1 = "a".buildURL("path") - assertEquals(null, url1) - - val url2 = "".buildURL("path") - assertEquals(null, url2) - - val url3 = "a.a".buildURL("path") - assertEquals(null, url3) - } - - @Test - fun test_build_URL_path_right() { - val url1 = "http://a.a".buildURL("path") - assertEquals("http://a.a/path", url1.toString()) - - val url2 = "http://a.a".buildURL("") - assertEquals("http://a.a", url2.toString()) - - val url3 = "http://a.a".buildURL("/path") - assertEquals("http://a.a/path", url3.toString()) - } - - @Test - fun test_build_URL_path_query_right() { - val url1 = "http://a.a".buildURL("path", "method=EMPTY", "class=android", "param3") - assertEquals("http://a.a/path?method=EMPTY&class=android¶m3", url1.toString()) - - val url2 = "http://a.a".buildURL("","method=EMPTY", "class=android", "param3") - assertEquals("http://a.a?method=EMPTY&class=android¶m3", url2.toString()) - - val url3 = "http://a.a".buildURL("") - assertEquals("http://a.a", url3.toString()) - } - @Test fun test_is_tennantId_not_valid() { @@ -219,13 +182,4 @@ class UrlExtensionTest { val url5 = tennant.setupURL("SANDBOX-EU-1") assertTrue(Pattern.compile(URL_REGEX).matcher(url5).matches()) } - - @Test - fun test_environment_concatenation() { - assertEquals("sandbox-eu-1", "sandbox" with "eu-1") - assertEquals("sandbox-eu-1", "sandbox" with "-eu-1") - assertEquals("live-eu-", "live" with "-eu-") - assertEquals("live-eu", "live" with "eu") - assertEquals("live", "live" with "") - } } \ No newline at end of file diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/VGSCollectTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/VGSCollectTest.kt index 3efcdc767..47ae29a3e 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/VGSCollectTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/VGSCollectTest.kt @@ -7,10 +7,10 @@ import com.verygoodsecurity.vgscollect.app.BaseTransmitActivity import com.verygoodsecurity.vgscollect.core.HTTPMethod import com.verygoodsecurity.vgscollect.core.VGSCollect import com.verygoodsecurity.vgscollect.core.VgsCollectResponseListener -import com.verygoodsecurity.vgscollect.core.api.ApiClient +import com.verygoodsecurity.vgscollect.core.api.client.ApiClient import com.verygoodsecurity.vgscollect.core.api.VgsApiTemporaryStorageImpl import com.verygoodsecurity.vgscollect.core.model.VGSHashMapWrapper -import com.verygoodsecurity.vgscollect.core.model.network.VGSRequest +import com.verygoodsecurity.vgscollect.core.model.network.* import com.verygoodsecurity.vgscollect.core.storage.InternalStorage import com.verygoodsecurity.vgscollect.core.storage.OnFieldStateChangeListener import com.verygoodsecurity.vgscollect.core.storage.content.file.TemporaryFileStorage @@ -20,12 +20,10 @@ import com.verygoodsecurity.vgscollect.view.internal.BaseInputField import com.verygoodsecurity.vgscollect.widget.* import org.junit.Assert.assertEquals import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito -import org.mockito.Mockito.after -import org.mockito.Mockito.spy +import org.mockito.ArgumentMatchers +import org.mockito.Mockito.* import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows @@ -45,11 +43,6 @@ class VGSCollectTest { activityController = Robolectric.buildActivity(Activity::class.java) activity = activityController.get() collect = VGSCollect(activity, "tnts") - - val activityShadow = Shadows.shadowOf(activity) - activityShadow.grantPermissions( - Manifest.permission.INTERNET - ) } @Test @@ -62,11 +55,11 @@ class VGSCollectTest { @Test fun test_remove_response_listener() { - val listener1 = Mockito.mock(VgsCollectResponseListener::class.java) + val listener1 = mock(VgsCollectResponseListener::class.java) collect.addOnResponseListeners(listener1) assertEquals(2, collect.getResponseListeners().size) // + analytic listener - val listener2 = Mockito.mock(VgsCollectResponseListener::class.java) + val listener2 = mock(VgsCollectResponseListener::class.java) collect.removeOnResponseListener(listener2) assertEquals(2, collect.getResponseListeners().size) // + analytic listener @@ -76,10 +69,10 @@ class VGSCollectTest { @Test fun test_remove_all_response_listeners() { - val listener1 = Mockito.mock(VgsCollectResponseListener::class.java) + val listener1 = mock(VgsCollectResponseListener::class.java) collect.addOnResponseListeners(listener1) assertEquals(2, collect.getResponseListeners().size) // + analytic listener - val listener2 = Mockito.mock(VgsCollectResponseListener::class.java) + val listener2 = mock(VgsCollectResponseListener::class.java) collect.addOnResponseListeners(listener2) assertEquals(3, collect.getResponseListeners().size) // + analytic listener @@ -94,28 +87,31 @@ class VGSCollectTest { applyStateChangeListener() applyStateChangeListener() - Mockito.verify(storage, Mockito.times(2)).attachStateChangeListener(any()) + verify(storage, times(2)).attachStateChangeListener(any()) } @Test fun test_bind_view() { val view = applyEditText(FieldType.INFO) - Mockito.verify(view, Mockito.times(2)).getFieldType() //default init + analytics, - Mockito.verify(view).getFieldName() - Mockito.verify(view).addStateListener(any()) + verify(view, times(2)).getFieldType() //default init + analytics, + verify(view).getFieldName() + verify(view).addStateListener(any()) } @Test fun test_on_destroy() { applyResponseListener() + val client = applyApiClient() val storage = applyStorage() collect.onDestroy() assertEquals(0, collect.getResponseListeners().size) - Mockito.verify(storage).clear() + verify(storage).clear() + + verify(client, after(500)).cancelAll() } @Test @@ -137,38 +133,197 @@ class VGSCollectTest { } @Test - fun test_submit_path_method() { + fun test_sync_path_method_no_internet_permission() { + val activityShadow = Shadows.shadowOf(activity) + activityShadow.grantPermissions( + Manifest.permission.ACCESS_NETWORK_STATE + ) + + val listener = applyResponseListener() + + collect.submit("/path", HTTPMethod.POST) + + verify(listener).onResponse(ArgumentMatchers.any(VGSResponse.ErrorResponse::class.java)) + } + + @Test + fun test_sync_path_method_no_access_network_state_permission() { + val activityShadow = Shadows.shadowOf(activity) + activityShadow.grantPermissions( + Manifest.permission.INTERNET + ) + + val listener = applyResponseListener() + + collect.submit("/path", HTTPMethod.POST) + + verify(listener).onResponse(ArgumentMatchers.any(VGSResponse.ErrorResponse::class.java)) + } + + @Test + fun test_sync_path_method() { + val activityShadow = Shadows.shadowOf(activity) + activityShadow.grantPermissions( + Manifest.permission.INTERNET, + Manifest.permission.ACCESS_NETWORK_STATE + ) + val client = applyApiClient() + doReturn(NetworkResponse()) + .`when`(client).execute(any()) + collect.submit("/path", HTTPMethod.POST) - Mockito.verify(client).call(any(), any(), any(), any()) + verify(client).execute(any()) } @Test - fun test_submit_request_builder() { + fun test_async_path_method_no_internet_permission() { + val activityShadow = Shadows.shadowOf(activity) + activityShadow.grantPermissions( + Manifest.permission.ACCESS_NETWORK_STATE + ) + + val listener = applyResponseListener() + + collect.asyncSubmit("/path", HTTPMethod.POST) + + verify(listener).onResponse(ArgumentMatchers.any(VGSResponse.ErrorResponse::class.java)) + } + + @Test + fun test_async_path_method_no_access_network_state_permission() { + val activityShadow = Shadows.shadowOf(activity) + activityShadow.grantPermissions( + Manifest.permission.INTERNET + ) + + val listener = applyResponseListener() + + collect.asyncSubmit("/path", HTTPMethod.POST) + + verify(listener).onResponse(ArgumentMatchers.any(VGSResponse.ErrorResponse::class.java)) + } + + @Test + fun test_async_path_method() { + val activityShadow = Shadows.shadowOf(activity) + activityShadow.grantPermissions( + Manifest.permission.INTERNET, + Manifest.permission.ACCESS_NETWORK_STATE + ) + val client = applyApiClient() + collect.asyncSubmit("/path", HTTPMethod.POST) + + verify(client, after(500)).enqueue(any(), any()) + } + + @Test + fun test_sync_request_builder_no_internet_permission() { + val activityShadow = Shadows.shadowOf(activity) + activityShadow.grantPermissions( + Manifest.permission.ACCESS_NETWORK_STATE + ) + + val listener = applyResponseListener() + + val request = VGSRequest.VGSRequestBuilder() + .setPath("/path") + .setMethod(HTTPMethod.POST) + .build() + collect.submit(request) + + verify(listener).onResponse(ArgumentMatchers.any(VGSResponse.ErrorResponse::class.java)) + } + + @Test + fun test_sync_request_builder_no_access_network_state_permission() { + val activityShadow = Shadows.shadowOf(activity) + activityShadow.grantPermissions( + Manifest.permission.INTERNET + ) + + val listener = applyResponseListener() + val request = VGSRequest.VGSRequestBuilder() .setPath("/path") .setMethod(HTTPMethod.POST) .build() collect.submit(request) - Mockito.verify(client).call(any(), any(), any(), any()) + verify(listener).onResponse(ArgumentMatchers.any(VGSResponse.ErrorResponse::class.java)) } @Test - fun test_asyncsubmit_path_method() { + fun test_sync_request_builder() { + val activityShadow = Shadows.shadowOf(activity) + activityShadow.grantPermissions( + Manifest.permission.INTERNET, + Manifest.permission.ACCESS_NETWORK_STATE + ) + val client = applyApiClient() - collect.asyncSubmit("/path", HTTPMethod.POST) + doReturn(NetworkResponse()) + .`when`(client).execute(any()) + + val request = VGSRequest.VGSRequestBuilder() + .setPath("/path") + .setMethod(HTTPMethod.POST) + .build() + collect.submit(request) + + verify(client).execute(any()) + } + - Mockito.verify(client, after(500)).call(any(), any(), any(), any()) + @Test + fun test_async_request_builder_no_internet_permission() { + val activityShadow = Shadows.shadowOf(activity) + activityShadow.grantPermissions( + Manifest.permission.ACCESS_NETWORK_STATE + ) + + val listener = applyResponseListener() + + val request = VGSRequest.VGSRequestBuilder() + .setPath("/path") + .setMethod(HTTPMethod.POST) + .build() + collect.asyncSubmit(request) + + verify(listener).onResponse(ArgumentMatchers.any(VGSResponse.ErrorResponse::class.java)) + } + + @Test + fun test_async_request_builder_no_access_network_state_permission() { + val activityShadow = Shadows.shadowOf(activity) + activityShadow.grantPermissions( + Manifest.permission.INTERNET + ) + + val listener = applyResponseListener() + + val request = VGSRequest.VGSRequestBuilder() + .setPath("/path") + .setMethod(HTTPMethod.POST) + .build() + collect.asyncSubmit(request) + + verify(listener).onResponse(ArgumentMatchers.any(VGSResponse.ErrorResponse::class.java)) } @Test - fun test_asyncsubmit_request_builder() { + fun test_async_request_builder() { + val activityShadow = Shadows.shadowOf(activity) + activityShadow.grantPermissions( + Manifest.permission.INTERNET, + Manifest.permission.ACCESS_NETWORK_STATE + ) + val client = applyApiClient() val request = VGSRequest.VGSRequestBuilder() @@ -177,7 +332,7 @@ class VGSCollectTest { .build() collect.asyncSubmit(request) - Mockito.verify(client, after(500)).call(any(), any(), any(), any()) + verify(client, after(500)).enqueue(any(), any()) } @Test @@ -190,7 +345,7 @@ class VGSCollectTest { collect.onActivityResult(TemporaryFileStorage.REQUEST_CODE, Activity.RESULT_OK, intent) - Mockito.verify(storage).getFileStorage() + verify(storage).getFileStorage() } @Test @@ -201,7 +356,7 @@ class VGSCollectTest { data["key"] = "value" collect.setCustomHeaders(data) - Mockito.verify(client).getTemporaryStorage() + verify(client).getTemporaryStorage() assertEquals(1, client.getTemporaryStorage().getCustomHeaders().size) } @@ -211,7 +366,7 @@ class VGSCollectTest { collect.resetCustomHeaders() - Mockito.verify(client).getTemporaryStorage() + verify(client).getTemporaryStorage() assertEquals(0, client.getTemporaryStorage().getCustomHeaders().size) } @@ -223,7 +378,7 @@ class VGSCollectTest { data["key"] = "value" collect.setCustomData(data) - Mockito.verify(client).getTemporaryStorage() + verify(client).getTemporaryStorage() assertEquals(1, client.getTemporaryStorage().getCustomData().size) } @@ -233,7 +388,7 @@ class VGSCollectTest { collect.resetCustomData() - Mockito.verify(client).getTemporaryStorage() + verify(client).getTemporaryStorage() assertEquals(0, client.getTemporaryStorage().getCustomData().size) } @@ -241,11 +396,9 @@ class VGSCollectTest { fun test_get_file_provider() { val storage = applyStorage() collect.getFileProvider() - Mockito.verify(storage).getFileProvider() + verify(storage).getFileProvider() } - private fun any(): T = Mockito.any() - private fun applyStorage(): InternalStorage { val storage = spy( InternalStorage(activity) ) collect.setStorage(storage) @@ -297,22 +450,22 @@ class VGSCollectTest { } private fun applyResponseListener(): VgsCollectResponseListener { - val listener = Mockito.mock(VgsCollectResponseListener::class.java) + val listener = mock(VgsCollectResponseListener::class.java) collect.addOnResponseListeners(listener) return listener } private fun applyStateChangeListener(): OnFieldStateChangeListener { - val listener = Mockito.mock(OnFieldStateChangeListener::class.java) + val listener = mock(OnFieldStateChangeListener::class.java) collect.addOnFieldStateChangeListener(listener) return listener } private fun applyApiClient(): ApiClient { - val client = Mockito.mock(ApiClient::class.java) - Mockito.doReturn(VgsApiTemporaryStorageImpl()) + val client = mock(ApiClient::class.java) + doReturn(VgsApiTemporaryStorageImpl()) .`when`(client).getTemporaryStorage() collect.setClient(client) diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/ApiClientTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/ApiClientTest.kt index 85a644f36..440138879 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/ApiClientTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/ApiClientTest.kt @@ -1,14 +1,15 @@ package com.verygoodsecurity.vgscollect.api import com.verygoodsecurity.vgscollect.core.* -import com.verygoodsecurity.vgscollect.core.api.ApiClient +import com.verygoodsecurity.vgscollect.core.api.client.ApiClient +import com.verygoodsecurity.vgscollect.core.model.network.VGSRequest import org.junit.Test import org.mockito.Mockito class ApiClientTest { @Test - fun test_Api_Call() { + fun test_api_execute() { val client = Mockito.mock(ApiClient::class.java) val headers = HashMap() @@ -16,9 +17,37 @@ class ApiClientTest { val data = HashMap() data.put("customData", "dataset") - client.call("/post", HTTPMethod.POST, headers, data) + val r = VGSRequest.VGSRequestBuilder() + .setPath("/post") + .setMethod(HTTPMethod.POST) + .setCustomData(data) + .setCustomHeader(headers) + .build() - Mockito.verify(client).call("/post", HTTPMethod.POST, headers, data) + client.execute(r) + + Mockito.verify(client).execute(r) + } + + @Test + fun test_api_enqueue() { + val client = Mockito.mock(ApiClient::class.java) + + val headers = HashMap() + headers.put("NEW-HEADER", "header") + val data = HashMap() + data.put("customData", "dataset") + + val r = VGSRequest.VGSRequestBuilder() + .setPath("/post") + .setMethod(HTTPMethod.POST) + .setCustomData(data) + .setCustomHeader(headers) + .build() + + client.enqueue(r) + + Mockito.verify(client).enqueue(r) } @Test diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/RequestTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/RequestTest.kt index 943597ce8..85186285b 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/RequestTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/RequestTest.kt @@ -8,7 +8,7 @@ import org.junit.Test class RequestTest { @Test - fun test_create_default_VGSRequest() { + fun test_create_default_request() { val r = VGSRequest.VGSRequestBuilder().build() assertEquals("/post", r.path) assertEquals(HTTPMethod.POST, r.method) @@ -17,8 +17,8 @@ class RequestTest { } @Test - fun test_create_VGSRequest_with_custom_data() { - val METHOD = HTTPMethod.GET + fun test_create_request_with_custom_data() { + val METHOD = HTTPMethod.POST val PATH = "/some/path" val headers = HashMap() headers["HEADER-S"] = "some-data" @@ -39,8 +39,8 @@ class RequestTest { } @Test - fun test_create_VGSRequest_without_custom_data() { - val METHOD = HTTPMethod.GET + fun test_create_request_without_custom_data() { + val METHOD = HTTPMethod.POST val PATH = "/some/path" val r = VGSRequest.VGSRequestBuilder() .setMethod(METHOD) diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/TLSSocketFactoryTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/TLSSocketFactoryTest.kt index aa35ea769..a8909dfcd 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/TLSSocketFactoryTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/TLSSocketFactoryTest.kt @@ -1,6 +1,6 @@ package com.verygoodsecurity.vgscollect.api -import com.verygoodsecurity.vgscollect.core.api.TLSSocketFactory +import com.verygoodsecurity.vgscollect.core.api.client.ssl.TLSSocketFactory import org.junit.Assert.assertTrue import org.junit.Test diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/client/extension/ResponseTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/client/extension/ResponseTest.kt new file mode 100644 index 000000000..01282322f --- /dev/null +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/client/extension/ResponseTest.kt @@ -0,0 +1,40 @@ +package com.verygoodsecurity.vgscollect.api.client.extension + +import com.verygoodsecurity.vgscollect.core.api.client.extension.isCodeSuccessful +import com.verygoodsecurity.vgscollect.core.api.client.extension.isHttpStatusCode +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class ResponseTest { + + @Test + fun test_is_code_successful() { + assertTrue(200.isCodeSuccessful()) + assertTrue(250.isCodeSuccessful()) + assertTrue(299.isCodeSuccessful()) + assertFalse(300.isCodeSuccessful()) + assertFalse(350.isCodeSuccessful()) + assertFalse(399.isCodeSuccessful()) + assertFalse(199.isCodeSuccessful()) + assertFalse(400.isCodeSuccessful()) + assertFalse(1500.isCodeSuccessful()) + } + + @Test + fun test_is_http_status_code() { + assertTrue(200.isHttpStatusCode()) + assertTrue(300.isHttpStatusCode()) + assertTrue(400.isHttpStatusCode()) + assertTrue(500.isHttpStatusCode()) + assertTrue(600.isHttpStatusCode()) + assertTrue(700.isHttpStatusCode()) + assertTrue(800.isHttpStatusCode()) + assertTrue(900.isHttpStatusCode()) + assertTrue(999.isHttpStatusCode()) + assertFalse(1000.isHttpStatusCode()) + assertFalse(100.isHttpStatusCode()) + assertFalse(0.isHttpStatusCode()) + assertFalse(199.isHttpStatusCode()) + } +} \ No newline at end of file diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/utils/extension/StringTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/utils/extension/StringTest.kt new file mode 100644 index 000000000..9cc89ccf1 --- /dev/null +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/utils/extension/StringTest.kt @@ -0,0 +1,27 @@ +package com.verygoodsecurity.vgscollect.utils.extension + +import com.verygoodsecurity.vgscollect.util.extension.concatWithDash +import com.verygoodsecurity.vgscollect.util.extension.concatWithSlash +import org.junit.Assert +import org.junit.Test + +class StringTest { + + @Test + fun test_concat_with_dash() { + Assert.assertEquals("sandbox-eu-1", "sandbox" concatWithDash "eu-1") + Assert.assertEquals("sandbox-eu-1", "sandbox" concatWithDash "-eu-1") + Assert.assertEquals("live-eu-", "live" concatWithDash "-eu-") + Assert.assertEquals("live-eu", "live" concatWithDash "eu") + Assert.assertEquals("live", "live" concatWithDash "") + } + + @Test + fun test_concat_with_slash() { + Assert.assertEquals("sandbox/path", "sandbox" concatWithSlash "path") + Assert.assertEquals("sandbox/path", "sandbox" concatWithSlash "/path") + Assert.assertEquals("live/path/", "live" concatWithSlash "/path/") + Assert.assertEquals("live/eu", "live" concatWithSlash "eu") + Assert.assertEquals("live", "live" concatWithSlash "") + } +} \ No newline at end of file From bdb5793174ec9e24cfc7aad990095fbd1810c0c2 Mon Sep 17 00:00:00 2001 From: Dmytro Kos Date: Tue, 24 Nov 2020 18:19:37 +0300 Subject: [PATCH 06/13] added ability to set custom hostname (#45) --- app/build.gradle | 6 +- .../demoapp/DemoApplication.kt | 2 + .../activity_case/VGSCollectActivity.kt | 6 +- .../main/res/layout/activity_collect_demo.xml | 2 +- .../vgscollect/core/HTTPMethod.kt | 5 + .../vgscollect/core/VGSCollect.kt | 121 +++++++++++++++--- .../vgscollect/core/api/UrlExtension.kt | 53 +++++--- .../vgscollect/core/api/VGSHttpBodyFormat.kt | 2 + .../core/api/analityc/CollectActionTracker.kt | 5 +- .../action/HostNameValidationAction.kt | 20 +++ .../core/api/analityc/utils/Boolean.kt | 9 ++ .../vgscollect/core/api/client/ApiClient.kt | 29 +++-- .../core/api/client/OkHttpClient.kt | 85 ++++++------ .../core/api/client/URLConnectionClient.kt | 95 +++++++++----- .../core/model/network/NetworkRequest.kt | 14 ++ .../core/model/network/VGSRequest.kt | 21 ++- .../vgscollect/util/Logger.kt | 6 + vgscollect/src/main/res/values/strings.xml | 1 + .../vgscollect/UrlExtensionTest.kt | 32 ++--- .../vgscollect/api/ApiClientTest.kt | 25 ++-- .../vgscollect/api/RequestTest.kt | 62 ++++++++- .../vgscollect/utils/extension/StringTest.kt | 64 +++++++-- 22 files changed, 504 insertions(+), 161 deletions(-) create mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/action/HostNameValidationAction.kt create mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/utils/Boolean.kt create mode 100644 vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/NetworkRequest.kt diff --git a/app/build.gradle b/app/build.gradle index 8fe914ca2..77179a6fd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,7 +7,7 @@ android { buildToolsVersion "29.0.3" defaultConfig { applicationId "com.verygoodsecurity.demoapp" - minSdkVersion 21 + minSdkVersion 19 targetSdkVersion 30 versionCode 1 versionName "1.0" @@ -76,7 +76,9 @@ dependencies { implementation project(":vgscollect") implementation project(":vgscollect-cardio") - implementation project(":vgscollect-bouncer") +// implementation project(":vgscollect-bouncer") + + implementation 'com.google.android.gms:play-services-auth:19.0.0' implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' diff --git a/app/src/main/java/com/verygoodsecurity/demoapp/DemoApplication.kt b/app/src/main/java/com/verygoodsecurity/demoapp/DemoApplication.kt index 43ac8342b..733472ab8 100644 --- a/app/src/main/java/com/verygoodsecurity/demoapp/DemoApplication.kt +++ b/app/src/main/java/com/verygoodsecurity/demoapp/DemoApplication.kt @@ -3,12 +3,14 @@ package com.verygoodsecurity.demoapp import android.app.Application import android.os.StrictMode import android.view.View +import com.google.android.gms.security.ProviderInstaller class DemoApplication: Application() { val leakedViews = mutableListOf() override fun onCreate() { super.onCreate() + ProviderInstaller.installIfNeeded(getApplicationContext()); enabledStrictMode() } diff --git a/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt b/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt index 5a8ffda86..6573b68b9 100644 --- a/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt +++ b/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt @@ -30,7 +30,6 @@ import com.verygoodsecurity.vgscollect.view.card.icon.CardIconAdapter import com.verygoodsecurity.vgscollect.view.card.validation.payment.ChecksumAlgorithm import com.verygoodsecurity.vgscollect.view.card.validation.rules.PaymentCardNumberRule import com.verygoodsecurity.vgscollect.view.card.validation.rules.PersonNameRule -import com.verygoodsecurity.vgscollect.view.date.DatePickerMode import kotlinx.android.synthetic.main.activity_collect_demo.* class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View.OnClickListener { @@ -219,7 +218,10 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. val envId = bndl?.getInt(StartActivity.ENVIROMENT, 0)?:0 env = Environment.values()[envId] - vgsForm = VGSCollect(this, vault_id, env) + vgsForm = VGSCollect.Builder(this, vault_id) + .setEnvironment(env) + .setHostname("collect-android-testing.verygoodsecurity.io/test") + .create() } override fun onCreateOptionsMenu(menu: Menu): Boolean { diff --git a/app/src/main/res/layout/activity_collect_demo.xml b/app/src/main/res/layout/activity_collect_demo.xml index c9a283eab..12497f76d 100644 --- a/app/src/main/res/layout/activity_collect_demo.xml +++ b/app/src/main/res/layout/activity_collect_demo.xml @@ -136,7 +136,7 @@ android:id="@+id/cardNumberField" android:layout_width="match_parent" android:layout_height="match_parent" - app:fieldName="card_data.cardNumber" + app:fieldName="cardNumber" style="@style/AppTheme.PaymentField" app:text="4111-1111-1111-1111" app:numberDivider="-" diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/HTTPMethod.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/HTTPMethod.kt index c2894e207..cf6870b41 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/HTTPMethod.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/HTTPMethod.kt @@ -7,6 +7,11 @@ package com.verygoodsecurity.vgscollect.core */ enum class HTTPMethod { + /** + * HTTP GET method + */ + GET, + /** * HTTP POST method */ diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt index 329c886d5..29e53e182 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt @@ -6,11 +6,13 @@ import android.content.Intent import android.os.Handler import android.os.Looper import androidx.annotation.VisibleForTesting +import com.verygoodsecurity.vgscollect.R import com.verygoodsecurity.vgscollect.app.BaseTransmitActivity import com.verygoodsecurity.vgscollect.core.api.* import com.verygoodsecurity.vgscollect.core.api.analityc.CollectActionTracker import com.verygoodsecurity.vgscollect.core.api.analityc.AnalyticTracker import com.verygoodsecurity.vgscollect.core.api.analityc.action.* +import com.verygoodsecurity.vgscollect.core.api.analityc.utils.toAnalyticStatus import com.verygoodsecurity.vgscollect.core.api.client.ApiClient import com.verygoodsecurity.vgscollect.core.api.client.extension.isHttpStatusCode import com.verygoodsecurity.vgscollect.core.model.VGSHashMapWrapper @@ -32,7 +34,6 @@ import com.verygoodsecurity.vgscollect.util.extension.isConnectionAvailable import com.verygoodsecurity.vgscollect.util.mapUsefulPayloads import com.verygoodsecurity.vgscollect.view.InputFieldView import com.verygoodsecurity.vgscollect.view.card.getAnalyticName -import java.security.MessageDigest import java.util.* import kotlin.collections.HashMap @@ -76,11 +77,9 @@ class VGSCollect { } } - private val baseURL: String + private var baseURL: String private val context: Context - private val isURLValid: Boolean - constructor( /** Activity context */ context: Context, @@ -100,8 +99,7 @@ class VGSCollect { ) baseURL = id.setupURL(environment.rawValue) - isURLValid = baseURL.isURLValid() - initializeCollect(baseURL) + initializeCollect() } constructor( @@ -136,13 +134,11 @@ class VGSCollect { ) baseURL = id.setupURL(environment) - isURLValid = baseURL.isURLValid() - initializeCollect(baseURL) + initializeCollect() } - private fun initializeCollect(baseURL: String) { + private fun initializeCollect() { client = ApiClient.newHttpClient() - client.setURL(baseURL) storage = InternalStorage(context, storageErrorListener) } @@ -258,7 +254,9 @@ class VGSCollect { var response: VGSResponse = VGSResponse.ErrorResponse() collectUserData(request) { - response = client.execute(request).toVGSResponse(context) + response = client.execute( + request.toNetworkRequest(baseURL) + ).toVGSResponse(context) } return response @@ -288,7 +286,7 @@ class VGSCollect { */ fun asyncSubmit(request: VGSRequest) { collectUserData(request) { - client.enqueue(request) { r -> + client.enqueue(request.toNetworkRequest(baseURL)) { r -> mainHandler.post { notifyAllListeners(r.toVGSResponse()) } } } @@ -298,7 +296,7 @@ class VGSCollect { when { !request.fieldsIgnore && !validateFields() -> return !request.fileIgnore && !validateFiles() -> return - !isURLValid -> notifyAllListeners(VGSError.URL_NOT_VALID.toVGSResponse(context)) + !baseURL.isURLValid() -> notifyAllListeners(VGSError.URL_NOT_VALID.toVGSResponse(context)) !context.hasInternetPermission() -> notifyAllListeners(VGSError.NO_INTERNET_PERMISSIONS.toVGSResponse(context)) !context.hasAccessNetworkStatePermission() -> @@ -312,7 +310,8 @@ class VGSCollect { !request.fileIgnore, !request.fieldsIgnore, request.customHeader.isNotEmpty(), - request.customData.isNotEmpty() + request.customData.isNotEmpty(), + hasCustomHostname ) submitRequest() } @@ -523,15 +522,17 @@ class VGSCollect { hasFields: Boolean = false, hasCustomHeader: Boolean = false, hasCustomData: Boolean = false, + hasCustomHostname: Boolean = false, code: Int = 200 ) { if (code.isHttpStatusCode()) { val m = with(mutableMapOf()) { - if (isSuccess) put("status", "Ok") else put("status", "Failed") + put("status", isSuccess.toAnalyticStatus()) put("statusCode", code) val arr = with(mutableListOf()) { + if (hasCustomHostname) add("customHostName") if (hasFiles) add("file") if (hasFields) add("fields") if (hasCustomHeader || @@ -579,4 +580,94 @@ class VGSCollect { AttachFileAction(m) ) } + + private var hasCustomHostname = false + + private fun configureHostname(host: String?, tnt: String) { + if (!host.isNullOrBlank()) { + val r = VGSRequest.VGSRequestBuilder() + .setMethod(HTTPMethod.GET) + .setFormat(VGSHttpBodyFormat.PLAIN_TEXT) + .build() + .toNetworkRequest( + host.toHostnameValidationUrl(tnt) + ) + + client.enqueue(r) { + hasCustomHostname = it.isSuccessful && host equalsUrl it.body + if (hasCustomHostname) { + client.setHost(it.body) + } else { + Logger.e(context, VGSCollect::class.java, R.string.error_custom_host_wrong) + } + + hostnameValidationEvent(hasCustomHostname, host) + } + } + } + + private fun hostnameValidationEvent( + isSuccess: Boolean, + hostname: String = "" + ) { + val m = with(mutableMapOf()) { + put("status", isSuccess.toAnalyticStatus()) + put("hostname", hostname) + + this + } + + tracker.logEvent( + HostNameValidationAction(m) + ) + } + + class Builder( + + /** Activity context */ + private val context: Context, + + /** Specific Vault ID */ + private val id: String + ) { + + private var environment: String = Environment.SANDBOX.rawValue + + private var host: String? = null + + /** Specify Environment for the VGSCollect instance. */ + fun setEnvironment(env: Environment, region: String = ""): Builder = this.apply { + environment = env.rawValue concatWithDash region + } + + /** + * Specify Environment for the VGSCollect instance. + * Also, Environment could be used with region prefix ( sandbox-eu-0 ). + */ + fun setEnvironment(env: String): Builder = this.apply { environment = env } + + /** Sets the VGSCollect instance to use the custom hostname. */ + fun setHostname(cname: String): Builder { + if (cname.isURLValid()) { + host = cname.toHost() + } + + if (host != cname) Logger.w( + VGSCollect::class.java, + "Hostname will be normalized to the $host" + ) + + return this + } + + /** + * Creates an VGSCollect with the arguments supplied to this + * builder. + */ + fun create(): VGSCollect { + return VGSCollect(context, id, environment).apply { + configureHostname(host, id) + } + } + } } \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/UrlExtension.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/UrlExtension.kt index 19eb7fdfc..8cc2540f8 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/UrlExtension.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/UrlExtension.kt @@ -7,7 +7,7 @@ import java.net.URL import java.util.regex.Pattern /** @suppress */ -internal fun String.setupURL(rawValue:String):String { +internal fun String.setupURL(rawValue: String): String { return when { this.isEmpty() || !isTennantIdValid() -> { Logger.e(VGSCollect::class.java, "tennantId is not valid") @@ -21,10 +21,10 @@ internal fun String.setupURL(rawValue:String):String { } } -private fun String.buildURL(env:String):String { +private fun String.buildURL(env: String): String { val DOMEN = "verygoodproxy.com" val DIVIDER = "." - val SCHEME = "https://" + val SCHEME = "https://" val builder = StringBuilder(SCHEME) .append(this).append(DIVIDER) @@ -34,23 +34,46 @@ private fun String.buildURL(env:String):String { return builder.toString() } -internal fun String.isTennantIdValid():Boolean { - val m = Pattern.compile("^[a-zA-Z0-9]*\$").matcher(this) +internal fun String.isTennantIdValid(): Boolean = + Pattern.compile("^[a-zA-Z0-9]*\$").matcher(this).matches() - return m.matches() +internal fun String.isEnvironmentValid(): Boolean = + Pattern.compile("^(live|sandbox|LIVE|SANDBOX)+((-)+([a-zA-Z0-9]+)|)+\$").matcher(this).matches() + +internal fun String.isURLValid(): Boolean { + return when { + isNullOrBlank() -> false + startsWith("http://") -> throw RuntimeException("Cleartext HTTP traffic to * not permitted") + else -> Pattern.compile( + "^(?:https?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\/.-]+)+[\\w\\:]+\$" + ).matcher(this).matches() + } } -internal fun String.isEnvironmentValid():Boolean { - val m = Pattern.compile("^(live|sandbox|LIVE|SANDBOX)+((-)+([a-zA-Z0-9]+)|)+\$").matcher(this) +internal fun String.toHostnameValidationUrl(tnt: String): String { + return String.format( + "https://js.verygoodvault.com/collect-configs/%s__%s.txt", + this.toHost(), + tnt + ) +} - return m.matches() +internal infix fun String.equalsUrl(name: String?): Boolean { + return toHost() == name?.toHost() } -internal fun String.isURLValid():Boolean { +internal fun String.toHttps(): String { + return when { + startsWith("http://") -> this + startsWith("https://") -> this + else -> "https://$this" + } +} + +internal fun String.toHost(): String { return try { - URL(this).toURI() - true - } catch (e:Exception) { - false + URL(this.toHttps()).host + } catch (e: MalformedURLException) { + "" } -} \ No newline at end of file +} diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/VGSHttpBodyFormat.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/VGSHttpBodyFormat.kt index 875957b9d..65174f0f6 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/VGSHttpBodyFormat.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/VGSHttpBodyFormat.kt @@ -1,9 +1,11 @@ package com.verygoodsecurity.vgscollect.core.api enum class VGSHttpBodyFormat { + PLAIN_TEXT, JSON } internal fun VGSHttpBodyFormat.toContentType() = when (this) { VGSHttpBodyFormat.JSON -> "application/json" + VGSHttpBodyFormat.PLAIN_TEXT -> "text/plain" } \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/CollectActionTracker.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/CollectActionTracker.kt index fd41bf0a7..d1678fe28 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/CollectActionTracker.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/CollectActionTracker.kt @@ -6,6 +6,7 @@ import com.verygoodsecurity.vgscollect.core.HTTPMethod import com.verygoodsecurity.vgscollect.core.api.client.ApiClient import com.verygoodsecurity.vgscollect.core.api.analityc.action.Action import com.verygoodsecurity.vgscollect.core.model.network.VGSRequest +import com.verygoodsecurity.vgscollect.core.model.network.toNetworkRequest import java.util.* import java.util.concurrent.Executors @@ -20,7 +21,7 @@ internal class CollectActionTracker( } private val client: ApiClient by lazy { - return@lazy ApiClient.newHttpClient(URL) + return@lazy ApiClient.newHttpClient(false) } override fun logEvent(action: Action) { @@ -77,7 +78,7 @@ internal class CollectActionTracker( .setCustomData(map) .build() - client.enqueue(r) + client.enqueue(r.toNetworkRequest(URL)) } companion object { diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/action/HostNameValidationAction.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/action/HostNameValidationAction.kt new file mode 100644 index 000000000..6e11710ad --- /dev/null +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/action/HostNameValidationAction.kt @@ -0,0 +1,20 @@ +package com.verygoodsecurity.vgscollect.core.api.analityc.action + +class HostNameValidationAction( + val params:Map +): Action { + + override fun getAttributes(): MutableMap { + return with(mutableMapOf()) { + putAll(params) + put(EVENT, INIT) + + this + } + } + + companion object { + private const val EVENT = "type" + private const val INIT = "HostNameValidation" + } +} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/utils/Boolean.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/utils/Boolean.kt new file mode 100644 index 000000000..1f85ed3ee --- /dev/null +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/analityc/utils/Boolean.kt @@ -0,0 +1,9 @@ +package com.verygoodsecurity.vgscollect.core.api.analityc.utils + +fun Boolean.toAnalyticStatus(): String { + return if(this) { + "Ok" + } else { + "Failed" + } +} \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/ApiClient.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/ApiClient.kt index d676d809f..98fdee6d6 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/ApiClient.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/ApiClient.kt @@ -2,16 +2,15 @@ package com.verygoodsecurity.vgscollect.core.api.client import android.os.Build import com.verygoodsecurity.vgscollect.core.api.VgsApiTemporaryStorage +import com.verygoodsecurity.vgscollect.core.model.network.NetworkRequest import com.verygoodsecurity.vgscollect.core.model.network.NetworkResponse -import com.verygoodsecurity.vgscollect.core.model.network.VGSRequest -import com.verygoodsecurity.vgscollect.core.model.network.VGSResponse internal interface ApiClient { - fun setURL(url: String) + fun setHost(url: String?) - fun enqueue(request: VGSRequest, callback: ((NetworkResponse) -> Unit)? = null) - fun execute(request: VGSRequest): NetworkResponse + fun enqueue(request: NetworkRequest, callback: ((NetworkResponse) -> Unit)? = null) + fun execute(request: NetworkRequest): NetworkResponse fun cancelAll() fun getTemporaryStorage(): VgsApiTemporaryStorage @@ -20,21 +19,27 @@ internal interface ApiClient { internal const val CONNECTION_TIME_OUT = 60000L internal const val AGENT = "vgs-client" internal const val CONTENT_TYPE = "Content-type" - internal const val TEMPORARY_AGENT_TEMPLATE = "source=androidSDK&medium=vgs-collect&content=%s&vgsCollectSessionId=%s" + internal const val TEMPORARY_AGENT_TEMPLATE = + "source=androidSDK&medium=vgs-collect&content=%s&vgsCollectSessionId=%s" - fun newHttpClient(url: String): ApiClient { + fun newHttpClient( + url: String, + isLogsVisible: Boolean = true + ): ApiClient { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - OkHttpClient().apply { setURL(url) } + OkHttpClient(isLogsVisible).apply { setHost(url) } } else { - URLConnectionClient.newInstance().apply { setURL(url) } + URLConnectionClient.newInstance(isLogsVisible).apply { setHost(url) } } } - fun newHttpClient(): ApiClient { + fun newHttpClient( + isLogsVisible: Boolean = true + ): ApiClient { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - OkHttpClient() + OkHttpClient(isLogsVisible) } else { - URLConnectionClient.newInstance() + URLConnectionClient.newInstance(isLogsVisible) } } } diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/OkHttpClient.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/OkHttpClient.kt index ee5f9ecfb..0863010ed 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/OkHttpClient.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/OkHttpClient.kt @@ -4,38 +4,40 @@ import com.verygoodsecurity.vgscollect.BuildConfig import com.verygoodsecurity.vgscollect.core.HTTPMethod import com.verygoodsecurity.vgscollect.core.VGSCollect import com.verygoodsecurity.vgscollect.core.api.* +import com.verygoodsecurity.vgscollect.core.api.analityc.CollectActionTracker import com.verygoodsecurity.vgscollect.core.api.client.ApiClient.Companion.AGENT import com.verygoodsecurity.vgscollect.core.api.client.ApiClient.Companion.CONNECTION_TIME_OUT import com.verygoodsecurity.vgscollect.core.api.client.ApiClient.Companion.TEMPORARY_AGENT_TEMPLATE -import com.verygoodsecurity.vgscollect.core.api.VgsApiTemporaryStorage -import com.verygoodsecurity.vgscollect.core.api.VgsApiTemporaryStorageImpl -import com.verygoodsecurity.vgscollect.core.api.analityc.CollectActionTracker import com.verygoodsecurity.vgscollect.core.api.client.extension.isCodeSuccessful import com.verygoodsecurity.vgscollect.core.api.client.extension.setMethod +import com.verygoodsecurity.vgscollect.core.model.network.NetworkRequest import com.verygoodsecurity.vgscollect.core.model.network.NetworkResponse import com.verygoodsecurity.vgscollect.core.model.network.VGSError -import com.verygoodsecurity.vgscollect.core.model.network.VGSRequest import com.verygoodsecurity.vgscollect.util.Logger -import com.verygoodsecurity.vgscollect.util.extension.concatWithSlash import com.verygoodsecurity.vgscollect.util.mapToJSON import okhttp3.* import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient -import okio.Buffer import java.io.IOException import java.io.InterruptedIOException -import java.lang.StringBuilder +import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException -internal class OkHttpClient : ApiClient { +internal class OkHttpClient( + private val isLogsVisible: Boolean = BuildConfig.DEBUG +) : ApiClient { + + private val hostInterceptor: HostInterceptor = HostInterceptor() private val client: OkHttpClient by lazy { OkHttpClient().newBuilder() - .addInterceptor(HttpLoggingInterceptor()) + .addInterceptor(HttpLoggingInterceptor(isLogsVisible)) + .addInterceptor(hostInterceptor) .callTimeout(CONNECTION_TIME_OUT, TimeUnit.MILLISECONDS) .readTimeout(CONNECTION_TIME_OUT, TimeUnit.MILLISECONDS) .writeTimeout(CONNECTION_TIME_OUT, TimeUnit.MILLISECONDS) + .dispatcher(Dispatcher(Executors.newSingleThreadExecutor())) .build() } @@ -43,41 +45,24 @@ internal class OkHttpClient : ApiClient { VgsApiTemporaryStorageImpl() } - private var baseUrl: String = "" - override fun setURL(url: String) { - baseUrl = url - } - - private fun printBody(okHttpRequest: Request) { //todo remove after implementation CNAme - try { - val copy: RequestBody = okHttpRequest.body!! - val buffer = Buffer() - copy.writeTo(buffer) - - Logger.i(OkHttpClient::class.java.canonicalName, "${buffer.readUtf8()}") - } catch (e: IOException) { - Logger.i(OkHttpClient::class.java.canonicalName, "no body") - } + override fun setHost(url: String?) { + hostInterceptor.host = url?.toHost() } - override fun enqueue(request: VGSRequest, callback: ((NetworkResponse) -> Unit)?) { - val url = (baseUrl concatWithSlash request.path) - - if (!url.isURLValid()) { + override fun enqueue(request: NetworkRequest, callback: ((NetworkResponse) -> Unit)?) { + if (!request.url.isURLValid()) { callback?.invoke(NetworkResponse(error = VGSError.URL_NOT_VALID)) return } val okHttpRequest = buildRequest( - url, + request.url, request.method, request.customHeader, request.customData, request.format ) - printBody(okHttpRequest) - try { client.newCall(okHttpRequest).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { @@ -103,15 +88,13 @@ internal class OkHttpClient : ApiClient { } } - override fun execute(request: VGSRequest): NetworkResponse { - val url = (baseUrl concatWithSlash request.path) - - if (!url.isURLValid()) { + override fun execute(request: NetworkRequest): NetworkResponse { + if (!request.url.isURLValid()) { return NetworkResponse(error = VGSError.URL_NOT_VALID) } val okHttpRequest = buildRequest( - url, + request.url, request.method, request.customHeader, request.customData, @@ -168,7 +151,31 @@ internal class OkHttpClient : ApiClient { return this } - class HttpLoggingInterceptor : Interceptor { + private class HostInterceptor : Interceptor { + var host: String? = null + override fun intercept(chain: Interceptor.Chain): Response { + val r = with(chain.request()) { + if (!host.isNullOrBlank() && host != url.host) { + val newUrl = chain.request().url.newBuilder() + .scheme(url.scheme) + .host(host!!) + .build() + + chain.request().newBuilder() + .url(newUrl) + .build() + } else { + this + } + } + + return chain.proceed(r) + } + } + + private class HttpLoggingInterceptor( + private val isLogsVisible: Boolean = BuildConfig.DEBUG + ) : Interceptor { companion object { private fun buildRequestLog(request: Request): String { @@ -197,10 +204,10 @@ internal class OkHttpClient : ApiClient { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() - Logger.i(VGSCollect::class.java.simpleName, buildRequestLog(request)) + if(isLogsVisible) Logger.i(VGSCollect::class.java.simpleName, buildRequestLog(request)) val response = chain.proceed(request) - Logger.i(VGSCollect::class.java.simpleName, buildResponseLog(response)) + if(isLogsVisible) Logger.i(VGSCollect::class.java.simpleName, buildResponseLog(response)) return response } diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/URLConnectionClient.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/URLConnectionClient.kt index c1feb0383..66d3b7bff 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/URLConnectionClient.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/api/client/URLConnectionClient.kt @@ -1,48 +1,48 @@ package com.verygoodsecurity.vgscollect.core.api.client import com.verygoodsecurity.vgscollect.BuildConfig +import com.verygoodsecurity.vgscollect.core.HTTPMethod import com.verygoodsecurity.vgscollect.core.VGSCollect import com.verygoodsecurity.vgscollect.core.api.* -import com.verygoodsecurity.vgscollect.core.api.client.ApiClient.Companion.AGENT -import com.verygoodsecurity.vgscollect.core.api.client.ApiClient.Companion.CONNECTION_TIME_OUT -import com.verygoodsecurity.vgscollect.core.api.client.ApiClient.Companion.CONTENT_TYPE -import com.verygoodsecurity.vgscollect.core.api.client.ssl.TLSSocketFactory import com.verygoodsecurity.vgscollect.core.api.VgsApiTemporaryStorage import com.verygoodsecurity.vgscollect.core.api.VgsApiTemporaryStorageImpl import com.verygoodsecurity.vgscollect.core.api.analityc.CollectActionTracker +import com.verygoodsecurity.vgscollect.core.api.client.ApiClient.Companion.AGENT +import com.verygoodsecurity.vgscollect.core.api.client.ApiClient.Companion.CONNECTION_TIME_OUT +import com.verygoodsecurity.vgscollect.core.api.client.ApiClient.Companion.CONTENT_TYPE import com.verygoodsecurity.vgscollect.core.api.client.extension.* -import com.verygoodsecurity.vgscollect.core.api.client.extension.callTimeout -import com.verygoodsecurity.vgscollect.core.api.client.extension.openConnection -import com.verygoodsecurity.vgscollect.core.api.client.extension.readTimeout -import com.verygoodsecurity.vgscollect.core.api.client.extension.setSSLSocketFactory +import com.verygoodsecurity.vgscollect.core.api.isURLValid +import com.verygoodsecurity.vgscollect.core.api.toContentType +import com.verygoodsecurity.vgscollect.core.model.network.NetworkRequest import com.verygoodsecurity.vgscollect.core.model.network.NetworkResponse import com.verygoodsecurity.vgscollect.core.model.network.VGSError -import com.verygoodsecurity.vgscollect.core.model.network.VGSRequest import com.verygoodsecurity.vgscollect.util.Logger import com.verygoodsecurity.vgscollect.util.extension.concatWithSlash import com.verygoodsecurity.vgscollect.util.mapToJSON import java.net.HttpURLConnection +import java.net.URL import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.Future -internal class URLConnectionClient : ApiClient { +internal class URLConnectionClient( + private val enableLogs: Boolean = BuildConfig.DEBUG +) : ApiClient { private val submittedTasks = mutableListOf>() private val executor: ExecutorService by lazy { Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) } - private var baseURL:String = "" - private val tempStore: VgsApiTemporaryStorage by lazy { VgsApiTemporaryStorageImpl() } - override fun setURL(url: String) { - baseURL = url + private var host: String? = null + override fun setHost(url: String?) { + host = url } - override fun enqueue(request: VGSRequest, callback: ((NetworkResponse) -> Unit)?) { + override fun enqueue(request: NetworkRequest, callback: ((NetworkResponse) -> Unit)?) { var task: Future<*>? = null task = executor.submit { try { @@ -55,32 +55,67 @@ internal class URLConnectionClient : ApiClient { submittedTasks.add(task) } - override fun execute(request: VGSRequest): NetworkResponse { - val url = (baseURL concatWithSlash request.path) - if(!url.isURLValid()) { - return NetworkResponse(error = VGSError.URL_NOT_VALID) + override fun execute(request: NetworkRequest): NetworkResponse { + return if (!request.url.isURLValid()) { + NetworkResponse(error = VGSError.URL_NOT_VALID) + } else { + makeRequest(request) } + } + + @Synchronized + private fun makeRequest(request: NetworkRequest): NetworkResponse { + return when (request.method) { + HTTPMethod.GET -> get(request) + HTTPMethod.POST -> post(request) + } + } + + private fun get(request: NetworkRequest): NetworkResponse { + val conn = generateURL(request).openConnection() + conn.requestMethod = request.method.toString() + conn.useCaches = false + + return handleResponse(conn) + } + + private fun generateURL(request: NetworkRequest): String { + return with(URL(request.url)) { + if (this@URLConnectionClient.host.isNullOrBlank() || this@URLConnectionClient.host == host) { + (host concatWithSlash path).toHttps() + } else { + (this@URLConnectionClient.host!! concatWithSlash path).toHttps() + } + } + } + private fun post(request: NetworkRequest): NetworkResponse { var connection: HttpURLConnection? = null return try { - connection = url.openConnection() - .setSSLSocketFactory(TLSSocketFactory()) + connection = generateURL(request).openConnection() .callTimeout(CONNECTION_TIME_OUT) .readTimeout(CONNECTION_TIME_OUT) .setInstanceFollowRedirectEnabled(false) .setIsUserInteractionEnabled(false) .setCacheEnabled(false) .addHeader(CONTENT_TYPE, request.format.toContentType()) - .addHeader(AGENT, String.format(ApiClient.TEMPORARY_AGENT_TEMPLATE, BuildConfig.VERSION_NAME, CollectActionTracker.Sid.id)) + .addHeader( + AGENT, + String.format( + ApiClient.TEMPORARY_AGENT_TEMPLATE, + BuildConfig.VERSION_NAME, + CollectActionTracker.Sid.id + ) + ) .addHeaders(request.customHeader) .setMethod(request.method) - Logger.i(VGSCollect::class.java.simpleName, buildRequestLog(connection)) + if (enableLogs) Logger.i(VGSCollect::class.java.simpleName, buildRequestLog(connection)) writeOutput(connection, request.customData) handleResponse(connection) } catch (e: Exception) { - Logger.e(VGSCollect::class.java, e.localizedMessage?:"") + if (enableLogs) Logger.e(VGSCollect::class.java, e.localizedMessage ?: "") NetworkResponse(message = e.localizedMessage) } finally { connection?.disconnect() @@ -99,14 +134,14 @@ internal class URLConnectionClient : ApiClient { private fun handleResponse(connection: HttpURLConnection): NetworkResponse { val responseCode = connection.responseCode - Logger.i(VGSCollect::class.java.simpleName, buildResponseLog(connection)) + if (enableLogs) Logger.i(VGSCollect::class.java.simpleName, buildResponseLog(connection)) return if (responseCode.isCodeSuccessful()) { val rawResponse = connection.inputStream?.bufferedReader()?.use { it.readText() } NetworkResponse(true, rawResponse, responseCode) } else { val responseStr = connection.errorStream?.bufferedReader()?.use { it.readText() } - Logger.e(VGSCollect::class.java, responseStr.toString()) + if (enableLogs) Logger.e(VGSCollect::class.java, responseStr.toString()) NetworkResponse(message = responseStr, code = responseCode) } } @@ -120,11 +155,11 @@ internal class URLConnectionClient : ApiClient { } companion object { - fun newInstance(): ApiClient { - return URLConnectionClient() + fun newInstance(isLogsVisible: Boolean = BuildConfig.DEBUG): ApiClient { + return URLConnectionClient(isLogsVisible) } - private fun buildRequestLog(connection: HttpURLConnection):String { + private fun buildRequestLog(connection: HttpURLConnection): String { val builder = StringBuilder("Request") .append("{") .append("method=") @@ -134,7 +169,7 @@ internal class URLConnectionClient : ApiClient { return builder.toString() } - private fun buildResponseLog(connection: HttpURLConnection):String { + private fun buildResponseLog(connection: HttpURLConnection): String { val builder = StringBuilder("Response") .append("{") .append("code=") diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/NetworkRequest.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/NetworkRequest.kt new file mode 100644 index 000000000..66a4a91a7 --- /dev/null +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/NetworkRequest.kt @@ -0,0 +1,14 @@ +package com.verygoodsecurity.vgscollect.core.model.network + +import com.verygoodsecurity.vgscollect.core.HTTPMethod +import com.verygoodsecurity.vgscollect.core.api.VGSHttpBodyFormat + +data class NetworkRequest( + val method: HTTPMethod, + var url: String, + val customHeader: MutableMap = mutableMapOf(), + val customData: MutableMap = mutableMapOf(), + val fieldsIgnore: Boolean = false, + val fileIgnore: Boolean = false, + val format: VGSHttpBodyFormat = VGSHttpBodyFormat.JSON, +) \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSRequest.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSRequest.kt index 04c8e27a7..0dba7d138 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSRequest.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/model/network/VGSRequest.kt @@ -2,6 +2,7 @@ package com.verygoodsecurity.vgscollect.core.model.network import com.verygoodsecurity.vgscollect.core.HTTPMethod import com.verygoodsecurity.vgscollect.core.api.VGSHttpBodyFormat +import com.verygoodsecurity.vgscollect.util.extension.concatWithSlash /** * Class to collect data before submit. @@ -31,7 +32,7 @@ data class VGSRequest private constructor( */ class VGSRequestBuilder { private var method: HTTPMethod = HTTPMethod.POST - private var path: String = "/post" + private var path: String = "" private val customHeader: HashMap = HashMap() private val customData: HashMap = HashMap() private var fieldsIgnore: Boolean = false @@ -103,7 +104,8 @@ data class VGSRequest private constructor( } internal fun setFormat(format: VGSHttpBodyFormat): VGSRequestBuilder { - return this.apply { this.format = format } + this.format = format + return this } /** @@ -129,8 +131,21 @@ data class VGSRequest private constructor( customHeader, customData, fieldsIgnore, - fileIgnore + fileIgnore, + format ) } } +} + +fun VGSRequest.toNetworkRequest(url: String): NetworkRequest { + return NetworkRequest( + method, + url concatWithSlash path, + customHeader, + customData, + fieldsIgnore, + fileIgnore, + format + ) } \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/Logger.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/Logger.kt index d0a2e8623..4e62983f4 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/Logger.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/util/Logger.kt @@ -38,6 +38,12 @@ internal object Logger { } } + fun w(clazz: Class<*>, message:String) { + if (BuildConfig.DEBUG) { + Log.w(clazz.canonicalName, message) + } + } + fun i(tag:String, message:String) { if(BuildConfig.DEBUG) { Log.i(tag, message) diff --git a/vgscollect/src/main/res/values/strings.xml b/vgscollect/src/main/res/values/strings.xml index f4c04a1b4..236a4c010 100644 --- a/vgscollect/src/main/res/values/strings.xml +++ b/vgscollect/src/main/res/values/strings.xml @@ -12,6 +12,7 @@ Divider for card number can\'t be greater than 1 symbol. (%1$s) Divider for card number can\'t a digit. (%1$s) The mask and card number maximum length are not equal for %1$s brand. + A specified host was not correct. 0123456789 ^[a-zA-Z0-9 ,\'.-]+$ diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/UrlExtensionTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/UrlExtensionTest.kt index e995dbea9..b1e37c1a1 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/UrlExtensionTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/UrlExtensionTest.kt @@ -1,8 +1,12 @@ package com.verygoodsecurity.vgscollect import com.verygoodsecurity.vgscollect.core.Environment -import com.verygoodsecurity.vgscollect.core.api.* -import org.junit.Assert.* +import com.verygoodsecurity.vgscollect.core.api.isEnvironmentValid +import com.verygoodsecurity.vgscollect.core.api.isTennantIdValid +import com.verygoodsecurity.vgscollect.core.api.isURLValid +import com.verygoodsecurity.vgscollect.core.api.setupURL +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test import java.util.regex.Pattern @@ -21,19 +25,19 @@ class UrlExtensionTest { assertFalse(testUrl2.isTennantIdValid()) val testUrl3 = "tnt com" - assertFalse( testUrl3.isTennantIdValid() ) + assertFalse(testUrl3.isTennantIdValid()) val testUrl4 = "2tnt/com" - assertFalse( testUrl4.isTennantIdValid() ) + assertFalse(testUrl4.isTennantIdValid()) val testUrl5 = "tnt:com" - assertFalse( testUrl5.isTennantIdValid() ) + assertFalse(testUrl5.isTennantIdValid()) val testUrl6 = "tnt*com" - assertFalse( testUrl6.isTennantIdValid() ) + assertFalse(testUrl6.isTennantIdValid()) val testUrl7 = "tnt?com" - assertFalse( testUrl7.isTennantIdValid() ) + assertFalse(testUrl7.isTennantIdValid()) } @Test fun test_is_tennantId_valid() { @@ -45,7 +49,7 @@ class UrlExtensionTest { @Test fun test_is_URL_valid() { - val url1 = "http://www.bla" + val url1 = "google.com" assertTrue(url1.isURLValid()) val url2 = "https://www.bla" @@ -53,18 +57,16 @@ class UrlExtensionTest { val url3 = "https://www.bla-one.com" assertTrue(url3.isURLValid()) - - val url4 = "http://www.example.com:8800" - assertTrue(url4.isURLValid()) } - @Test + @Test(expected = RuntimeException::class) fun test_is_url_not_valid() { - val url1 = "www.bla.com" + val url1 = "http://www.bla" assertFalse(url1.isURLValid()) - val url2 = "google.com" - assertFalse(url2.isURLValid()) + val url4 = "http://www.example.com:8800" + assertFalse(url4.isURLValid()) + val url3 = "http://www.ex ample.com:8800" assertFalse(url3.isURLValid()) diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/ApiClientTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/ApiClientTest.kt index 440138879..ed70dda4c 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/ApiClientTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/ApiClientTest.kt @@ -2,6 +2,7 @@ package com.verygoodsecurity.vgscollect.api import com.verygoodsecurity.vgscollect.core.* import com.verygoodsecurity.vgscollect.core.api.client.ApiClient +import com.verygoodsecurity.vgscollect.core.model.network.NetworkRequest import com.verygoodsecurity.vgscollect.core.model.network.VGSRequest import org.junit.Test import org.mockito.Mockito @@ -17,12 +18,12 @@ class ApiClientTest { val data = HashMap() data.put("customData", "dataset") - val r = VGSRequest.VGSRequestBuilder() - .setPath("/post") - .setMethod(HTTPMethod.POST) - .setCustomData(data) - .setCustomHeader(headers) - .build() + val r = NetworkRequest( + HTTPMethod.POST, + "https://www.test.com/post", + headers, + data + ) client.execute(r) @@ -38,12 +39,12 @@ class ApiClientTest { val data = HashMap() data.put("customData", "dataset") - val r = VGSRequest.VGSRequestBuilder() - .setPath("/post") - .setMethod(HTTPMethod.POST) - .setCustomData(data) - .setCustomHeader(headers) - .build() + val r = NetworkRequest( + HTTPMethod.POST, + "https://www.test.com/post", + headers, + data + ) client.enqueue(r) diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/RequestTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/RequestTest.kt index 85186285b..2d419eaa0 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/RequestTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/api/RequestTest.kt @@ -1,7 +1,7 @@ package com.verygoodsecurity.vgscollect.api import com.verygoodsecurity.vgscollect.core.HTTPMethod -import com.verygoodsecurity.vgscollect.core.model.network.VGSRequest +import com.verygoodsecurity.vgscollect.core.model.network.* import org.junit.Assert.assertEquals import org.junit.Test @@ -10,7 +10,7 @@ class RequestTest { @Test fun test_create_default_request() { val r = VGSRequest.VGSRequestBuilder().build() - assertEquals("/post", r.path) + assertEquals("", r.path) assertEquals(HTTPMethod.POST, r.method) assertEquals(HashMap(), r.customData) assertEquals(HashMap(), r.customHeader) @@ -51,4 +51,62 @@ class RequestTest { assertEquals(METHOD, r.method) } + @Test + fun test_to_network_request() { + val BASE_URL = "base.url" + val METHOD = HTTPMethod.POST + val PATH = "/some/path" + + val exampleRequest = NetworkRequest( + METHOD, + BASE_URL+PATH + ) + + val r = VGSRequest.VGSRequestBuilder() + .setMethod(METHOD) + .setPath(PATH) + .build().toNetworkRequest(BASE_URL) + + assertEquals(exampleRequest, r) + } + + @Test + fun test_to_response_success() { + val BODY = "data" + val CODE = 200 + + val exampleRequest = VGSResponse.SuccessResponse( + rawResponse = BODY, + successCode = CODE + ) + + val r = NetworkResponse( + true, + BODY, + CODE + ).toVGSResponse() as VGSResponse.SuccessResponse + + assertEquals(exampleRequest.rawResponse, r.rawResponse) + assertEquals(exampleRequest.successCode, r.successCode) + } + + @Test + fun test_to_response_failed() { + val MESSAGE = "error text" + val CODE = 401 + + val exampleRequest = VGSResponse.ErrorResponse( + MESSAGE, + CODE + ) + + val r = NetworkResponse( + code = CODE, + message = MESSAGE + ).toVGSResponse() as VGSResponse.ErrorResponse + + assertEquals(exampleRequest.localizeMessage, r.localizeMessage) + assertEquals(exampleRequest.code, r.code) + } + } \ No newline at end of file diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/utils/extension/StringTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/utils/extension/StringTest.kt index 9cc89ccf1..8e0b6fd98 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/utils/extension/StringTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/utils/extension/StringTest.kt @@ -1,27 +1,69 @@ package com.verygoodsecurity.vgscollect.utils.extension +import com.verygoodsecurity.vgscollect.core.api.equalsUrl +import com.verygoodsecurity.vgscollect.core.api.toHost +import com.verygoodsecurity.vgscollect.core.api.toHostnameValidationUrl +import com.verygoodsecurity.vgscollect.core.api.toHttps import com.verygoodsecurity.vgscollect.util.extension.concatWithDash import com.verygoodsecurity.vgscollect.util.extension.concatWithSlash -import org.junit.Assert +import org.junit.Assert.* import org.junit.Test class StringTest { @Test fun test_concat_with_dash() { - Assert.assertEquals("sandbox-eu-1", "sandbox" concatWithDash "eu-1") - Assert.assertEquals("sandbox-eu-1", "sandbox" concatWithDash "-eu-1") - Assert.assertEquals("live-eu-", "live" concatWithDash "-eu-") - Assert.assertEquals("live-eu", "live" concatWithDash "eu") - Assert.assertEquals("live", "live" concatWithDash "") + assertEquals("sandbox-eu-1", "sandbox" concatWithDash "eu-1") + assertEquals("sandbox-eu-1", "sandbox" concatWithDash "-eu-1") + assertEquals("live-eu-", "live" concatWithDash "-eu-") + assertEquals("live-eu", "live" concatWithDash "eu") + assertEquals("live", "live" concatWithDash "") } @Test fun test_concat_with_slash() { - Assert.assertEquals("sandbox/path", "sandbox" concatWithSlash "path") - Assert.assertEquals("sandbox/path", "sandbox" concatWithSlash "/path") - Assert.assertEquals("live/path/", "live" concatWithSlash "/path/") - Assert.assertEquals("live/eu", "live" concatWithSlash "eu") - Assert.assertEquals("live", "live" concatWithSlash "") + assertEquals("sandbox/path", "sandbox" concatWithSlash "path") + assertEquals("sandbox/path", "sandbox" concatWithSlash "/path") + assertEquals("live/path/", "live" concatWithSlash "/path/") + assertEquals("live/eu", "live" concatWithSlash "eu") + assertEquals("live", "live" concatWithSlash "") + } + + @Test + fun test_to_hostname_validation_url() { + val test1 = "www.vgs.com".toHostnameValidationUrl("tnt") + assertEquals("https://js.verygoodvault.com/collect-configs/www.vgs.com__tnt.txt", test1) + + val test2 = "".toHostnameValidationUrl("") + assertEquals("https://js.verygoodvault.com/collect-configs/__.txt", test2) + } + + @Test + fun test_equals_url() { + assertTrue("www.vgs.com" equalsUrl "www.vgs.com") + assertTrue("http://www.vgs.com" equalsUrl "www.vgs.com") + assertTrue("www.vgs.com" equalsUrl "http://www.vgs.com") + assertTrue("http://www.vgs.com" equalsUrl "http://www.vgs.com") + assertTrue("https://www.vgs.com" equalsUrl "www.vgs.com") + assertTrue("www.vgs.com" equalsUrl "https://www.vgs.com") + assertTrue("https://www.vgs.com" equalsUrl "https://www.vgs.com") + + assertFalse("vgs.com" equalsUrl "https://www.vgs.com") + assertFalse("vgs.com" equalsUrl "http://www.vgs.com") + assertFalse("vgs.com" equalsUrl "www.vgs.com") + } + + @Test + fun test_to_https() { + assertEquals("https://www.vgs.io", "https://www.vgs.io".toHttps()) + assertEquals("http://www.vgs.io", "http://www.vgs.io".toHttps()) + assertEquals("https://www.vgs.io", "www.vgs.io".toHttps()) + } + + @Test + fun test_to_host() { + assertEquals("www.vgs.io", "https://www.vgs.io".toHost()) + assertEquals("www.vgs.io", "http://www.vgs.io".toHost()) + assertEquals("www.vgs.io", "www.vgs.io".toHost()) } } \ No newline at end of file From 86187e38fbaa0162aab2e16c6d35f68f622fc611 Mon Sep 17 00:00:00 2001 From: Dmytro Kos Date: Thu, 26 Nov 2020 16:03:36 +0300 Subject: [PATCH 07/13] Updated support for Maestro card numbers (#46) --- .../vgscollect/core/VGSCollect.kt | 5 +++++ .../vgscollect/view/card/CardType.kt | 2 +- .../card/filter/brand/MaestroTest.kt | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt index 29e53e182..02737fbc1 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt @@ -640,6 +640,11 @@ class VGSCollect { environment = env.rawValue concatWithDash region } + /** Specify Environment for the VGSCollect instance. */ + fun setEnvironment(env: Environment): Builder = this.apply { + environment = env.rawValue + } + /** * Specify Environment for the VGSCollect instance. * Also, Environment could be used with region prefix ( sandbox-eu-0 ). diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/CardType.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/CardType.kt index b120a4a06..9cab7e300 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/CardType.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/CardType.kt @@ -39,7 +39,7 @@ enum class CardType(val regex:String, ), MAESTRO( - "^(5018|5020|5038|6304|6390[0-9]{2}|67[0-9]{4})", + "^(5018|5020|5038|56|57|58|6304|6390[0-9]{2}|67[0-9]{4})", R.drawable.ic_maestro_dark, "#### #### #### ####", ChecksumAlgorithm.LUHN, diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/card/filter/brand/MaestroTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/card/filter/brand/MaestroTest.kt index 05f435459..52ae95b44 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/card/filter/brand/MaestroTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/card/filter/brand/MaestroTest.kt @@ -69,4 +69,22 @@ class MaestroTest { val brand = filter.detect("50201232") Assert.assertEquals(brand?.name, CardType.MAESTRO.name) } + + @Test + fun test_10() { + val brand = filter.detect("56111111") + Assert.assertEquals(brand?.name, CardType.MAESTRO.name) + } + + @Test + fun test_11() { + val brand = filter.detect("57111111") + Assert.assertEquals(brand?.name, CardType.MAESTRO.name) + } + + @Test + fun test_12() { + val brand = filter.detect("58111111") + Assert.assertEquals(brand?.name, CardType.MAESTRO.name) + } } \ No newline at end of file From 52044818fc9e0da473ec5eaf25c08bd44d35bbbf Mon Sep 17 00:00:00 2001 From: Dmytro Kos Date: Thu, 26 Nov 2020 18:41:59 +0300 Subject: [PATCH 08/13] Improved file runtime caching (#47) --- .../activity_case/VGSCollectActivity.kt | 35 +++++++------ .../core/storage/content/file/Base64Cipher.kt | 20 ++++---- .../content/file/TemporaryFileStorage.kt | 46 ++++++++++++----- .../storage/content/file/VGSFileProvider.kt | 7 +++ .../storage/content/file/VgsFileCipher.kt | 6 +-- .../storage/file/Base64CipherTest.kt | 4 +- .../storage/file/FileStorageTest.kt | 51 ++++++++++++------- 7 files changed, 109 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt b/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt index 6573b68b9..ab670cb6b 100644 --- a/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt +++ b/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt @@ -70,7 +70,7 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. vgsForm.bindView(cardExpDateField) cardExpDateField?.setOnFieldStateChangeListener(object : OnFieldStateChangeListener { override fun onStateChange(state: FieldState) { - if(!state.isEmpty && !state.isValid && !state.hasFocus) { + if (!state.isEmpty && !state.isValid && !state.hasFocus) { cardExpDateFieldLay?.setError("fill it please") } else { cardExpDateFieldLay?.setError(null) @@ -91,7 +91,7 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. vgsForm.bindView(cardHolderField) cardHolderField?.setOnFieldStateChangeListener(object : OnFieldStateChangeListener { override fun onStateChange(state: FieldState) { - if(!state.isEmpty && !state.isValid && !state.hasFocus) { + if (!state.isEmpty && !state.isValid && !state.hasFocus) { cardHolderFieldLay?.setError("fill it please") } else { cardHolderFieldLay?.setError(null) @@ -104,7 +104,7 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. vgsForm.bindView(cardCVCField) cardCVCField?.setOnFieldStateChangeListener(object : OnFieldStateChangeListener { override fun onStateChange(state: FieldState) { - if(!state.isEmpty && !state.isValid && !state.hasFocus) { + if (!state.isEmpty && !state.isValid && !state.hasFocus) { cardCVCFieldLay?.setError("fill it please") } else { cardCVCFieldLay?.setError(null) @@ -167,7 +167,7 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. cardNumberField.setCardIconAdapter(object : CardIconAdapter(this) { override fun getIcon(cardType: CardType, name: String?, resId: Int, r: Rect): Drawable { - return if(cardType == CardType.VISA) { + return if (cardType == CardType.VISA) { getDrawable(R.drawable.ic_visa_light) } else { super.getIcon(cardType, name, resId, r) @@ -182,7 +182,7 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. bin: String, mask: String ): String { - return when(cardType) { + return when (cardType) { CardType.UNKNOWN -> { if (bin == "7771") { "# # # #" @@ -213,7 +213,7 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. val bndl = intent?.extras vault_id = bndl?.getString(StartActivity.VAULT_ID, "")?:"" - path = bndl?.getString(StartActivity.PATH,"/")?:"" + path = bndl?.getString(StartActivity.PATH, "/")?:"" val envId = bndl?.getInt(StartActivity.ENVIROMENT, 0)?:0 env = Environment.values()[envId] @@ -222,6 +222,9 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. .setEnvironment(env) .setHostname("collect-android-testing.verygoodsecurity.io/test") .create() + + val cacheSize = 10 * 1024 * 1024 // 10MB + vgsForm.getFileProvider().resize(cacheSize) } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -308,12 +311,12 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. setStateLoading(false) when (response) { - is VGSResponse.SuccessResponse -> responseContainerView.text = response.toString() - is VGSResponse.ErrorResponse -> responseContainerView.text = response.toString() + is VGSResponse.SuccessResponse -> responseContainerView.text = "Code: ${response.successCode}" + is VGSResponse.ErrorResponse -> responseContainerView.text = "Code: ${response.errorCode}" } } - private fun setStateLoading(state:Boolean) { + private fun setStateLoading(state: Boolean) { if(state) { progressBar?.visibility = View.VISIBLE submitBtn?.isEnabled = false @@ -325,16 +328,16 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. } } - private fun setEnabledResponseHeader(isEnabled:Boolean) { + private fun setEnabledResponseHeader(isEnabled: Boolean) { if(isEnabled) { - attachBtn.setTextColor(ContextCompat.getColor(this, - R.color.state_active - )) + attachBtn.setTextColor( + ContextCompat.getColor(this, R.color.state_active) + ) } else { responseContainerView.text = "" - attachBtn.setTextColor(ContextCompat.getColor(this, - R.color.state_unactive - )) + attachBtn.setTextColor( + ContextCompat.getColor(this, R.color.state_unactive) + ) } } diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/Base64Cipher.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/Base64Cipher.kt index 68a2b1dfe..396b171cc 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/Base64Cipher.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/Base64Cipher.kt @@ -8,10 +8,7 @@ import android.provider.BaseColumns import android.provider.DocumentsContract import android.util.Base64 import androidx.annotation.VisibleForTesting -import java.io.File -import java.io.FileOutputStream -import java.io.InputStream -import java.io.OutputStream +import java.io.* import java.util.* internal class Base64Cipher(val context: Context):VgsFileCipher { @@ -47,13 +44,18 @@ internal class Base64Cipher(val context: Context):VgsFileCipher { return result } override fun retrieve(map: HashMap):Pair? { - val uri = map[submitCode.toString()]?.toString() + val uri = map[submitCode.toString()]?.toString().run { + if(isNullOrEmpty()) { + null + } else { + Uri.parse(this) + } + } val pair = when { - uri.isNullOrEmpty() -> null - DocumentsContract.isDocumentUri(context, Uri.parse(uri)) && - documentUriExists(Uri.parse(uri)) -> uri to fieldName - contentUriExists(Uri.parse(uri)) -> uri to fieldName + uri == null -> null + DocumentsContract.isDocumentUri(context, uri) && documentUriExists(uri) -> fieldName to uri.toString() + contentUriExists(uri) -> fieldName to uri.toString() else -> null } reset() diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt index 518a4654d..a8a6db94c 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle +import android.util.LruCache import androidx.annotation.VisibleForTesting import com.verygoodsecurity.vgscollect.app.FilePickerActivity import com.verygoodsecurity.vgscollect.core.model.network.VGSError @@ -21,15 +22,27 @@ internal class TemporaryFileStorage( internal const val REQUEST_CODE = 0x3712 } - private var cipher:VgsFileCipher = Base64Cipher(context) + private var cipher: VgsFileCipher = Base64Cipher(context) - private val store = mutableMapOf>() + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal val memoryCache:LruCache by lazy { + val totalMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt() + val cacheSize = totalMemory / 8 + object : LruCache(cacheSize) {} + } + + private val store = mutableMapOf() override fun detachAll() { store.clear() + memoryCache.evictAll() + } + + override fun resize(size: Int) { + memoryCache.resize(size) } - override fun attachFile(fieldName : String) { + override fun attachFile(fieldName: String) { val tag = cipher.save(fieldName) val mRequestFileIntent = Intent(context, FilePickerActivity::class.java) @@ -38,53 +51,60 @@ internal class TemporaryFileStorage( putString(FilePickerActivity.TAG, tag.toString()) } mRequestFileIntent.putExtras(bndl) - (context as Activity).startActivityForResult(mRequestFileIntent, + (context as Activity).startActivityForResult( + mRequestFileIntent, REQUEST_CODE ) } - override fun getAttachedFiles():List = store.keys.toList() + override fun getAttachedFiles(): List = store.keys.toList() override fun detachFile(file: FileState) { store.remove(file) + memoryCache.remove(file.fieldName) } override fun dispatch(map: HashMap) { val fileInfo = cipher.retrieve(map) - if(fileInfo == null) { + fileInfo?.second?.let { + val base64Content = cipher.getBase64(Uri.parse(it)) + memoryCache.put(fileInfo.first, base64Content) + } + + if (fileInfo == null) { errorListener?.onStorageError(VGSError.FILE_NOT_FOUND) } else { - addItem(fileInfo.second, fileInfo.first) + addItem(fileInfo.first, fileInfo.second) } } override fun clear() { store.clear() + memoryCache.evictAll() } override fun addItem(fieldName: String, uriStr: String) { val fileInfo = Uri.parse(uriStr).parseFile(context, fieldName) fileInfo?.let { store.clear() - store[fileInfo] = Pair(it.fieldName, uriStr) + store[fileInfo] = it.fieldName } } override fun getItems(): MutableCollection { - return store.values.unzip().first.toMutableList() + return store.values } override fun getAssociatedList(): MutableCollection> { return store.values.map { - val uri = Uri.parse(it.second) - val fileBase64 = cipher.getBase64(uri) - it.first to fileBase64 + val s = memoryCache.get(it) + it to memoryCache.get(it) }.toMutableList() } @VisibleForTesting - fun setCipher(c:VgsFileCipher) { + fun setCipher(c: VgsFileCipher) { cipher = c } } \ No newline at end of file diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/VGSFileProvider.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/VGSFileProvider.kt index 3a64b3f56..84658d7a7 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/VGSFileProvider.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/VGSFileProvider.kt @@ -8,6 +8,13 @@ import com.verygoodsecurity.vgscollect.core.model.state.FileState */ interface VGSFileProvider { + /** + * Specify the maximum size of the cache for file stored before submit to the Proxy Server. + * + * @param cacheSize The new maximum size. + */ + fun resize(cacheSize: Int) + /** * Mentioned below method allows to attach file to the temporary local file storage before * its sending to the Server. diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/VgsFileCipher.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/VgsFileCipher.kt index 2de761163..c24579df5 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/VgsFileCipher.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/VgsFileCipher.kt @@ -4,8 +4,8 @@ import android.net.Uri import java.util.HashMap internal interface VgsFileCipher { - fun save(fieldName:String):Long - fun retrieve(map: HashMap):Pair? + fun save(fieldName: String): Long + fun retrieve(map: HashMap): Pair? - fun getBase64(file:Uri):String + fun getBase64(file: Uri): String } \ No newline at end of file diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/file/Base64CipherTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/file/Base64CipherTest.kt index 5e99730a7..ee683d6ed 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/file/Base64CipherTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/file/Base64CipherTest.kt @@ -64,7 +64,7 @@ class Base64CipherTest { val p = cipher.retrieve(map) - assertEquals(fieldName, p?.second) - assertEquals(uri, p?.first) + assertEquals(fieldName, p?.first) + assertEquals(uri, p?.second) } } \ No newline at end of file diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/file/FileStorageTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/file/FileStorageTest.kt index 3522edd20..56bae9dd0 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/file/FileStorageTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/file/FileStorageTest.kt @@ -7,13 +7,13 @@ import com.verygoodsecurity.vgscollect.core.storage.content.file.FileStorage import com.verygoodsecurity.vgscollect.core.storage.content.file.TemporaryFileStorage import com.verygoodsecurity.vgscollect.core.storage.content.file.VGSFileProvider import com.verygoodsecurity.vgscollect.core.storage.content.file.VgsFileCipher +import io.mockk.spyk import org.junit.Assert.assertEquals import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito -import org.mockito.Mockito.mock +import org.mockito.Mockito.* import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.android.controller.ActivityController @@ -29,11 +29,9 @@ class FileStorageTest { activity = activityController.get() } - private fun any(): T = Mockito.any() - @Test fun test_add_item() { - val store:VgsStore = TemporaryFileStorage(activity) + val store: VgsStore = TemporaryFileStorage(activity) store.addItem("userData.response", "file:///tmp/user.txt") assertEquals(1, store.getItems().size) @@ -50,7 +48,7 @@ class FileStorageTest { @Test fun test_clear() { - val store:VgsStore = TemporaryFileStorage(activity) + val store: VgsStore = TemporaryFileStorage(activity) store.addItem("userData.response", "file:///tmp/user.txt") assertEquals(1, store.getItems().size) @@ -61,7 +59,7 @@ class FileStorageTest { @Test fun test_get_items() { - val store:VgsStore = TemporaryFileStorage(activity) + val store: VgsStore = TemporaryFileStorage(activity) store.addItem("userData.response", "file:///tmp/user.txt") @@ -71,43 +69,59 @@ class FileStorageTest { @Test fun test_get_associated_list() { val c = mock(VgsFileCipher::class.java) - val fieldName = "userData.response" + val base64Str = "base64Str" + val fieldName = "userData.response" val filePath = "file:///tmp/user.txt" + val retMap = HashMap() + retMap[fieldName] = filePath + val uri = Uri.parse(filePath) - Mockito.doReturn(base64Str) + doReturn(base64Str) .`when`(c).getBase64(uri) + + val response = fieldName to filePath + + doReturn(response) + .`when`(c).retrieve(retMap) + val store: FileStorage = with(TemporaryFileStorage(activity)) { this.setCipher(c) this } - store.addItem(fieldName, filePath) - val list = store.getAssociatedList() + val map = HashMap() + map[fieldName] = filePath + store.dispatch(map) - assertEquals(1, list.size) - Mockito.verify(c).getBase64(uri) - val p: Pair = list.toMutableList()[0] + val p = store.getAssociatedList().toMutableList()[0] assertEquals(fieldName, p.first) assertEquals(base64Str, p.second) } - + @Test fun test_dispatch() { val c = mock(VgsFileCipher::class.java) + val base64Str = "base64Str" val fieldName = "userData.response" val filePath = "file:///tmp/user.txt" val retMap = HashMap() retMap[fieldName] = filePath + val uri = Uri.parse(filePath) + + doReturn(base64Str) + .`when`(c).getBase64(uri) + + val response = fieldName to filePath - Mockito.doReturn(response) + doReturn(response) .`when`(c).retrieve(retMap) val store: FileStorage = with(TemporaryFileStorage(activity)) { @@ -120,6 +134,9 @@ class FileStorageTest { map[fieldName] = filePath store.dispatch(map) + + verify(c).getBase64(uri) + assertEquals(1, store.getItems().size) } @@ -137,7 +154,7 @@ class FileStorageTest { provider.attachFile(fieldName) - Mockito.verify(c).save(fieldName) + verify(c).save(fieldName) } @Test From 5a2523f191a98cd782fb8958b52b2b963f355d69 Mon Sep 17 00:00:00 2001 From: Dmytro Kos Date: Mon, 30 Nov 2020 17:02:16 +0300 Subject: [PATCH 09/13] Added possibility to unsubscribe from a View updates (#48) --- .../activity_case/VGSCollectActivity.kt | 7 ++++- .../vgscollect/core/VGSCollect.kt | 11 ++++++++ .../core/storage/InternalStorage.kt | 26 ++++++++++++------- .../vgscollect/core/storage/VgsStore.kt | 5 ++++ .../content/field/TemporaryFieldsStorage.kt | 4 +++ .../content/file/TemporaryFileStorage.kt | 18 ++++++++----- .../view/AccessibilityStatePreparer.kt | 2 ++ .../vgscollect/view/InputFieldView.kt | 11 +++++++- .../card/conection/BaseInputConnection.kt | 4 +++ .../card/conection/InputCardCVCConnection.kt | 12 ++++----- .../conection/InputCardExpDateConnection.kt | 10 +++---- .../conection/InputCardHolderConnection.kt | 10 +++---- .../conection/InputCardNumberConnection.kt | 24 ++++++++--------- .../card/conection/InputInfoConnection.kt | 10 +++---- .../view/card/conection/InputSSNConnection.kt | 10 +++---- .../vgscollect/VGSCollectTest.kt | 18 ++++++++++++- .../storage/fields/FieldsStorageTest.kt | 11 ++++++++ .../storage/file/FileStorageTest.kt | 15 +++++++++++ .../view/card/InputFieldViewTest.kt | 1 - .../card/number/VGSCardNumberEditTextTest.kt | 5 +--- 20 files changed, 151 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt b/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt index ab670cb6b..2dd347772 100644 --- a/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt +++ b/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt @@ -288,6 +288,11 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. } override fun onDestroy() { + vgsForm.unbindView(cardNumberField) + vgsForm.unbindView(cardCVCField) + vgsForm.unbindView(cardExpDateField) + vgsForm.unbindView(cardHolderField) + vgsForm.onDestroy() super.onDestroy() } @@ -312,7 +317,7 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. when (response) { is VGSResponse.SuccessResponse -> responseContainerView.text = "Code: ${response.successCode}" - is VGSResponse.ErrorResponse -> responseContainerView.text = "Code: ${response.errorCode}" + is VGSResponse.ErrorResponse -> responseContainerView.text = response.toString() } } diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt index 02737fbc1..5e2dbf4bc 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/VGSCollect.kt @@ -198,6 +198,17 @@ class VGSCollect { initField(view) } + /** + * Allows to unsubscribe from a View updates. + * + * @param view base class for VGS secure fields. + */ + fun unbindView(view: InputFieldView?) { + view?.let { + storage.unsubscribe(view) + } + } + /** * This method adds a listener whose methods are called whenever VGS secure fields state changes. * diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/InternalStorage.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/InternalStorage.kt index f5d611b6c..e50e899f4 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/InternalStorage.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/InternalStorage.kt @@ -21,7 +21,7 @@ internal class InternalStorage( private val fileProvider: VGSFileProvider private val fileStorage: FileStorage - private val fieldsStorage:VgsStore + private val fieldsStorage: VgsStore private val emitter: IStateEmitter init { @@ -42,22 +42,22 @@ internal class InternalStorage( } fun getFileProvider() = fileProvider - fun getAttachedFiles() = fileProvider.getAttachedFiles() + fun getAttachedFiles() = fileProvider.getAttachedFiles() fun getFileStorage() = fileStorage fun getFieldsStorage() = fieldsStorage - fun getFieldsStates():MutableCollection = fieldsStorage.getItems() + fun getFieldsStates(): MutableCollection = fieldsStorage.getItems() fun getAssociatedList( fieldsIgnore: Boolean = false, fileIgnore: Boolean = false - ):MutableCollection> { + ): MutableCollection> { val list = mutableListOf>() - if(fieldsIgnore.not()) { + if (fieldsIgnore.not()) { list.addAll(fieldsStorage.getItems().toAssociatedList()) } - if(fileIgnore.not()) { + if (fileIgnore.not()) { list.merge(fileStorage.getAssociatedList()) } @@ -74,14 +74,20 @@ internal class InternalStorage( } fun performSubscription(view: InputFieldView?) { - view?.statePreparer?.let { + view?.let { fieldsDependencyDispatcher.addDependencyListener( - view.getFieldType(), - it.getDependencyListener() + it.getFieldType(), + it.statePreparer.getDependencyListener() ) + it.addStateListener(emitter.performSubscription()) } + } - view?.addStateListener(emitter.performSubscription()) + fun unsubscribe(view: InputFieldView?) { + view?.let { + it.statePreparer.unsubscribe() + fieldsStorage.remove(it.statePreparer.getView().id) + } } fun getFileSizeLimit(): Int { diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/VgsStore.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/VgsStore.kt index 3ad93d665..ba9eff746 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/VgsStore.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/VgsStore.kt @@ -10,6 +10,11 @@ internal interface VgsStore { */ fun clear() + /** + * Remove field from storage + */ + fun remove(id: K) + /** * Appends the specified state to the storage. */ diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/field/TemporaryFieldsStorage.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/field/TemporaryFieldsStorage.kt index 2440d047e..c14732acc 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/field/TemporaryFieldsStorage.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/field/TemporaryFieldsStorage.kt @@ -30,6 +30,10 @@ internal class TemporaryFieldsStorage( store.clear() } + override fun remove(id: Int) { + store.remove(id) + } + override fun getItems(): MutableCollection { return store.values } diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt index a8a6db94c..7fbedbb7a 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt @@ -31,7 +31,7 @@ internal class TemporaryFileStorage( object : LruCache(cacheSize) {} } - private val store = mutableMapOf() + private val store = mutableMapOf() override fun detachAll() { store.clear() @@ -57,10 +57,10 @@ internal class TemporaryFileStorage( ) } - override fun getAttachedFiles(): List = store.keys.toList() + override fun getAttachedFiles(): List = store.values.toList() override fun detachFile(file: FileState) { - store.remove(file) + store.remove(file.fieldName) memoryCache.remove(file.fieldName) } @@ -84,21 +84,25 @@ internal class TemporaryFileStorage( memoryCache.evictAll() } + override fun remove(key: String) { + memoryCache.remove(key) + store.remove(key) + } + override fun addItem(fieldName: String, uriStr: String) { val fileInfo = Uri.parse(uriStr).parseFile(context, fieldName) fileInfo?.let { store.clear() - store[fileInfo] = it.fieldName + store[it.fieldName] = fileInfo } } override fun getItems(): MutableCollection { - return store.values + return store.keys } override fun getAssociatedList(): MutableCollection> { - return store.values.map { - val s = memoryCache.get(it) + return store.keys.map { it to memoryCache.get(it) }.toMutableList() } diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/AccessibilityStatePreparer.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/AccessibilityStatePreparer.kt index eecdbb794..746954f78 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/AccessibilityStatePreparer.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/AccessibilityStatePreparer.kt @@ -6,7 +6,9 @@ import com.verygoodsecurity.vgscollect.core.storage.DependencyListener /** @suppress */ internal interface AccessibilityStatePreparer { + fun getId(): Int fun getView(): View + fun unsubscribe() fun getDependencyListener(): DependencyListener fun setAnalyticTracker(tr: AnalyticTracker) diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/InputFieldView.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/InputFieldView.kt index 976685255..5413b4b96 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/InputFieldView.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/InputFieldView.kt @@ -131,16 +131,25 @@ abstract class InputFieldView @JvmOverloads constructor( } } - internal val statePreparer = StatePreparer() + internal val statePreparer:AccessibilityStatePreparer = StatePreparer() internal inner class StatePreparer:AccessibilityStatePreparer { + + override fun getId(): Int = inputField.id + override fun getView():View { return inputField } + override fun getDependencyListener(): DependencyListener = notifier + override fun setAnalyticTracker(tr: AnalyticTracker) { inputField.tracker = tr } + + override fun unsubscribe() { + inputField.stateListener = null + } } override fun onDetachedFromWindow() { diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/BaseInputConnection.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/BaseInputConnection.kt index c3bd773be..2aba12823 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/BaseInputConnection.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/BaseInputConnection.kt @@ -7,6 +7,10 @@ internal abstract class BaseInputConnection: InputRunnable { private var stateListeners = mutableListOf() + protected fun clearAllListeners() { + stateListeners.clear() + } + protected fun addNewListener(listener: OnVgsViewStateChangeListener) { if(!stateListeners.contains(listener)) { stateListeners.add(listener) diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardCVCConnection.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardCVCConnection.kt index b1972ac37..14f598360 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardCVCConnection.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardCVCConnection.kt @@ -7,9 +7,9 @@ import com.verygoodsecurity.vgscollect.view.card.validation.VGSValidator /** @suppress */ internal class InputCardCVCConnection( - private val id:Int, + private val id: Int, validator: VGSValidator? -): BaseInputConnection() { +) : BaseInputConnection() { internal var runtimeValidator: VGSValidator? = validator private var output = VGSFieldState() @@ -21,7 +21,7 @@ internal class InputCardCVCConnection( override fun getOutput() = output override fun setOutputListener(l: OnVgsViewStateChangeListener?) { - l?.let { addNewListener(it) } + l?.let { addNewListener(it) } ?: clearAllListeners() } override fun run() { @@ -40,12 +40,12 @@ internal class InputCardCVCConnection( } private fun checkIsContentValid(content: String?): Boolean { - val updatedStr = content?.trim()?:"" + val updatedStr = content?.trim() ?: "" - return runtimeValidator?.isValid(updatedStr)?:false + return runtimeValidator?.isValid(updatedStr) ?: false } - private fun isRequiredValid():Boolean { + private fun isRequiredValid(): Boolean { return output.isRequired && !output.content?.data.isNullOrEmpty() || !output.isRequired } diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardExpDateConnection.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardExpDateConnection.kt index 41373c970..6bc4ae738 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardExpDateConnection.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardExpDateConnection.kt @@ -7,9 +7,9 @@ import com.verygoodsecurity.vgscollect.view.card.validation.VGSValidator /** @suppress */ internal class InputCardExpDateConnection( - private val id:Int, + private val id: Int, private vararg val validators: VGSValidator -): BaseInputConnection() { +) : BaseInputConnection() { private var output = VGSFieldState() override fun setOutput(state: VGSFieldState) { @@ -19,7 +19,7 @@ internal class InputCardExpDateConnection( override fun getOutput() = output override fun setOutputListener(listener: OnVgsViewStateChangeListener?) { - listener?.let { addNewListener(it) } + listener?.let { addNewListener(it) } ?: clearAllListeners() } override fun run() { @@ -37,7 +37,7 @@ internal class InputCardExpDateConnection( } } - private fun checkIsContentValid(content:String?):Boolean { + private fun checkIsContentValid(content: String?): Boolean { val updatedStr = content?.trim() ?: "" var isDateValid = true @@ -49,7 +49,7 @@ internal class InputCardExpDateConnection( return isDateValid } - private fun isRequiredValid():Boolean { + private fun isRequiredValid(): Boolean { return output.isRequired && !output.content?.data.isNullOrEmpty() || !output.isRequired } diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardHolderConnection.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardHolderConnection.kt index 67598f91c..110c86fd1 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardHolderConnection.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardHolderConnection.kt @@ -7,9 +7,9 @@ import com.verygoodsecurity.vgscollect.view.card.validation.VGSValidator /** @suppress */ internal class InputCardHolderConnection( - private val id:Int, + private val id: Int, private val validator: VGSValidator? -): BaseInputConnection() { +) : BaseInputConnection() { private var output = VGSFieldState() @@ -20,7 +20,7 @@ internal class InputCardHolderConnection( override fun getOutput() = output override fun setOutputListener(listener: OnVgsViewStateChangeListener?) { - listener?.let { addNewListener(it) } + listener?.let { addNewListener(it) } ?: clearAllListeners() } override fun run() { @@ -39,9 +39,9 @@ internal class InputCardHolderConnection( } private fun checkIsContentValid(content: String?): Boolean { - val updatedStr = content?.trim()?:"" + val updatedStr = content?.trim() ?: "" - return validator?.isValid(updatedStr)?:false + return validator?.isValid(updatedStr) ?: false } private fun isRequiredValid(): Boolean { diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardNumberConnection.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardNumberConnection.kt index cc31eb9b5..fc693d48b 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardNumberConnection.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputCardNumberConnection.kt @@ -11,11 +11,11 @@ import com.verygoodsecurity.vgscollect.view.card.validation.payment.brand.* /** @suppress */ internal class InputCardNumberConnection( - private val id:Int, + private val id: Int, private val validator: VGSValidator?, private val IcardBrand: IDrawCardBrand? = null, - private val divider:String? = null -): BaseInputConnection() { + private val divider: String? = null +) : BaseInputConnection() { private val cardFilters = mutableListOf() @@ -30,7 +30,7 @@ internal class InputCardNumberConnection( override fun setOutputListener(listener: OnVgsViewStateChangeListener?) { listener?.let { addNewListener(it) - } + } ?: clearAllListeners() } override fun clearFilters() { @@ -61,11 +61,11 @@ internal class InputCardNumberConnection( output.isValid = isRequiredRuleValid && isContentRuleValid } - private fun isRequiredValid():Boolean { + private fun isRequiredValid(): Boolean { return output.isRequired && !output.content?.data.isNullOrEmpty() || !output.isRequired } - private fun isContentValid(card: CardBrandPreview):Boolean { + private fun isContentValid(card: CardBrandPreview): Boolean { val content = output.content?.data return when { !output.isRequired && content.isNullOrEmpty() -> true @@ -79,13 +79,13 @@ internal class InputCardNumberConnection( ): Boolean { val rawStr = output.content?.data?.replace(divider ?: " ", "") ?: "" - val isValid = if(card.successfullyDetected) { + val isValid = if (card.successfullyDetected) { val isLengthAppropriate = checkLength(card.numberLength, rawStr.length) val isLuhnValid: Boolean = validateCheckSum(card.algorithm, rawStr) isLengthAppropriate && isLuhnValid } else { - validator?.isValid(rawStr)?:false + validator?.isValid(rawStr) ?: false } return isValid @@ -94,8 +94,8 @@ internal class InputCardNumberConnection( private fun validateCheckSum( algorithm: ChecksumAlgorithm, cardNumber: String - ):Boolean { - return when(algorithm) { + ): Boolean { + return when (algorithm) { ChecksumAlgorithm.LUHN -> LuhnCheckSumValidator().isValid(cardNumber) ChecksumAlgorithm.NONE -> true else -> false @@ -113,10 +113,10 @@ internal class InputCardNumberConnection( } private fun detectBrand(): CardBrandPreview { - for(i in cardFilters.indices) { + for (i in cardFilters.indices) { val filter = cardFilters[i] val brand = filter.detect(output.content?.data) - if(brand != null) { + if (brand != null) { return brand } } diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputInfoConnection.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputInfoConnection.kt index 44a155178..e60d848b1 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputInfoConnection.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputInfoConnection.kt @@ -7,9 +7,9 @@ import com.verygoodsecurity.vgscollect.view.card.validation.VGSValidator /** @suppress */ internal class InputInfoConnection( - private val id:Int, + private val id: Int, private val validator: VGSValidator? -): BaseInputConnection() { +) : BaseInputConnection() { private var output = VGSFieldState() override fun setOutput(state: VGSFieldState) { @@ -19,7 +19,7 @@ internal class InputInfoConnection( override fun getOutput() = output override fun setOutputListener(l: OnVgsViewStateChangeListener?) { - l?.let { addNewListener(it) } + l?.let { addNewListener(it) } ?: clearAllListeners() } override fun run() { @@ -38,9 +38,9 @@ internal class InputInfoConnection( } private fun checkIsContentValid(content: String?): Boolean { - val updatedStr = content?.trim()?:"" + val updatedStr = content?.trim() ?: "" - return validator?.isValid(updatedStr)?:false + return validator?.isValid(updatedStr) ?: false } private fun isRequiredValid(): Boolean { diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputSSNConnection.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputSSNConnection.kt index 32e25f4d5..e0f2f9697 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputSSNConnection.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/view/card/conection/InputSSNConnection.kt @@ -6,9 +6,9 @@ import com.verygoodsecurity.vgscollect.view.card.filter.VGSCardFilter import com.verygoodsecurity.vgscollect.view.card.validation.VGSValidator internal class InputSSNConnection( - private val id:Int, + private val id: Int, private val validator: VGSValidator? -): BaseInputConnection() { +) : BaseInputConnection() { private var output = VGSFieldState() override fun setOutput(state: VGSFieldState) { @@ -18,7 +18,7 @@ internal class InputSSNConnection( override fun getOutput() = output override fun setOutputListener(l: OnVgsViewStateChangeListener?) { - l?.let { addNewListener(it) } + l?.let { addNewListener(it) } ?: clearAllListeners() } override fun run() { @@ -44,9 +44,9 @@ internal class InputSSNConnection( } private fun checkIsContentValid(content: String?): Boolean { - val updatedStr = content?.trim()?:"" + val updatedStr = content?.trim() ?: "" - return validator?.isValid(updatedStr)?:false + return validator?.isValid(updatedStr) ?: false } private fun isRequiredValid(): Boolean { diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/VGSCollectTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/VGSCollectTest.kt index 47ae29a3e..35af08dff 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/VGSCollectTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/VGSCollectTest.kt @@ -18,7 +18,7 @@ import com.verygoodsecurity.vgscollect.view.InputFieldView import com.verygoodsecurity.vgscollect.view.card.FieldType import com.verygoodsecurity.vgscollect.view.internal.BaseInputField import com.verygoodsecurity.vgscollect.widget.* -import org.junit.Assert.assertEquals +import org.junit.Assert.* import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -99,6 +99,22 @@ class VGSCollectTest { verify(view).addStateListener(any()) } + @Test + fun test_unbind_view() { + val view = applyEditText(FieldType.INFO) + assertEquals(1, collect.getAllStates().size) + assertTrue(view.statePreparer.getView() is BaseInputField) + assertNotNull((view.statePreparer.getView() as BaseInputField).stateListener) + + collect.unbindView(view) + assertEquals(0, collect.getAllStates().size) + assertNull((view.statePreparer.getView() as BaseInputField).stateListener) + + view.setText("SDS") + assertEquals(0, collect.getAllStates().size) + assertNull((view.statePreparer.getView() as BaseInputField).stateListener) + } + @Test fun test_on_destroy() { applyResponseListener() diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/fields/FieldsStorageTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/fields/FieldsStorageTest.kt index 7610717a0..71a52c59b 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/fields/FieldsStorageTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/fields/FieldsStorageTest.kt @@ -62,6 +62,17 @@ class FieldsStorageTest { assertEquals(2, store.getItems().size) } + @Test + fun test_remove_item() { + store.addItem(0, VGSFieldState(isFocusable = false, fieldName = "n1")) + + assertEquals(1, store.getItems().size) + + store.remove(0) + + assertEquals(0, store.getItems().size) + } + @Test fun test_perform_subscription() { val listener = store.performSubscription() diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/file/FileStorageTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/file/FileStorageTest.kt index 56bae9dd0..789330884 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/file/FileStorageTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/storage/file/FileStorageTest.kt @@ -2,6 +2,7 @@ package com.verygoodsecurity.vgscollect.storage.file import android.app.Activity import android.net.Uri +import com.verygoodsecurity.vgscollect.core.model.state.VGSFieldState import com.verygoodsecurity.vgscollect.core.storage.VgsStore import com.verygoodsecurity.vgscollect.core.storage.content.file.FileStorage import com.verygoodsecurity.vgscollect.core.storage.content.file.TemporaryFileStorage @@ -46,6 +47,20 @@ class FileStorageTest { assertEquals(1, store.getItems().size) } + @Test + fun test_remove_item() { + val KEY = "file" + val store: VgsStore = TemporaryFileStorage(activity) + + store.addItem(KEY, "file:///tmp/user.txt") + + assertEquals(1, store.getItems().size) + + store.remove(KEY) + + assertEquals(0, store.getItems().size) + } + @Test fun test_clear() { val store: VgsStore = TemporaryFileStorage(activity) diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/view/card/InputFieldViewTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/view/card/InputFieldViewTest.kt index b40891af6..a2224313e 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/view/card/InputFieldViewTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/view/card/InputFieldViewTest.kt @@ -16,7 +16,6 @@ import org.junit.Assert import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.Robolectric diff --git a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/view/card/number/VGSCardNumberEditTextTest.kt b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/view/card/number/VGSCardNumberEditTextTest.kt index 59a3412be..67e3a1af2 100644 --- a/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/view/card/number/VGSCardNumberEditTextTest.kt +++ b/vgscollect/src/test/java/com/verygoodsecurity/vgscollect/view/card/number/VGSCardNumberEditTextTest.kt @@ -6,6 +6,7 @@ import android.view.Gravity import android.view.View import com.verygoodsecurity.vgscollect.R import com.verygoodsecurity.vgscollect.TestApplication +import com.verygoodsecurity.vgscollect.any import com.verygoodsecurity.vgscollect.core.model.state.FieldState import com.verygoodsecurity.vgscollect.core.storage.OnFieldStateChangeListener import com.verygoodsecurity.vgscollect.view.card.FieldType @@ -18,7 +19,6 @@ import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito @@ -540,7 +540,4 @@ class VGSCardNumberEditTextTest { assertEquals(false, state3!!.isValid) assertEquals(19, state3.contentLength) } - - - private fun any(): T = Mockito.any() } \ No newline at end of file From 396096175b60a90ef178d306bfec5e9326979752 Mon Sep 17 00:00:00 2001 From: Dmytro Kos Date: Mon, 30 Nov 2020 18:12:43 +0300 Subject: [PATCH 10/13] update gradle version (#49) --- build.gradle | 6 +++--- gradle/wrapper/gradle-wrapper.properties | 2 +- vgscollect/build.gradle | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 126814ac3..64cef692f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.4.10' + ext.kotlin_version = '1.4.20' ext.dokka_version = '1.4.0-rc' repositories { @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:4.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5' @@ -18,7 +18,7 @@ buildscript { } ext { - mockito_version = '3.5.0' + mockito_version = '3.6.0' android_support_libraries = '1.2.0' } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index acd884c8c..c618062ff 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/vgscollect/build.gradle b/vgscollect/build.gradle index d2983f623..a84baacf7 100644 --- a/vgscollect/build.gradle +++ b/vgscollect/build.gradle @@ -71,13 +71,13 @@ tasks.withType(Javadoc).all { dependencies { implementation "androidx.appcompat:appcompat:$android_support_libraries" implementation "com.google.android.material:material:1.2.1" - implementation 'androidx.core:core-ktx:1.3.1' + implementation 'androidx.core:core-ktx:1.3.2' implementation 'com.squareup.okhttp3:okhttp:4.8.0' - testImplementation 'junit:junit:4.13' + testImplementation 'junit:junit:4.13.1' testImplementation "org.mockito:mockito-core:$mockito_version" testImplementation "org.mockito:mockito-inline:$mockito_version" - testImplementation 'org.json:json:20200518' + testImplementation 'org.json:json:20201115' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' testImplementation "org.robolectric:robolectric:4.4" From c41a037aea9b49d86fc1f612184a3236ea43e70e Mon Sep 17 00:00:00 2001 From: Dmytro Kos Date: Mon, 30 Nov 2020 18:13:00 +0300 Subject: [PATCH 11/13] increase Collect module version (#50) --- vgscollect/gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vgscollect/gradle.properties b/vgscollect/gradle.properties index af3cf0a8f..3641b8d99 100644 --- a/vgscollect/gradle.properties +++ b/vgscollect/gradle.properties @@ -3,5 +3,5 @@ POM_DESCRIPTION=VGS Collect - is a product suite that allows customers to collec POM_BINTRAY_NAME=vgscollect POM_ARTIFACT_ID=vgscollect POM_PACKAGING=aar -POM_VERSION=1.2.5 -CODE_VERSION=1205 \ No newline at end of file +POM_VERSION=1.3.0 +CODE_VERSION=1300 \ No newline at end of file From b0f92ae316de6b9c75bcacd8593863f7d157f9e3 Mon Sep 17 00:00:00 2001 From: Dmytro Kos Date: Mon, 30 Nov 2020 18:54:38 +0300 Subject: [PATCH 12/13] Update Bouncer SDK version (#51) --- vgscollect-bouncer/build.gradle | 5 ++--- vgscollect-bouncer/gradle.properties | 4 ++-- .../verygoodsecurity/api/bouncer/ScanActivity.kt | 14 +++++--------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/vgscollect-bouncer/build.gradle b/vgscollect-bouncer/build.gradle index c3a3a0e59..d7be9e129 100644 --- a/vgscollect-bouncer/build.gradle +++ b/vgscollect-bouncer/build.gradle @@ -38,14 +38,13 @@ android { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "androidx.appcompat:appcompat:$android_support_libraries" - implementation 'androidx.core:core-ktx:1.3.1' + implementation 'androidx.core:core-ktx:1.3.2' dokkaHtmlPlugin("org.jetbrains.dokka:dokka-base:$dokka_version") api project(':vgscollect') - implementation 'com.getbouncer:cardscan-ui:2.0.0018' + implementation 'com.getbouncer:cardscan-ui:2.0.0055' } apply from: rootProject.file('release-bintray.gradle') \ No newline at end of file diff --git a/vgscollect-bouncer/gradle.properties b/vgscollect-bouncer/gradle.properties index 178b0d701..ffd5bdcc6 100644 --- a/vgscollect-bouncer/gradle.properties +++ b/vgscollect-bouncer/gradle.properties @@ -3,5 +3,5 @@ POM_DESCRIPTION=CardScan - fast and modern SDK for scanning Card Numbers. Integr POM_BINTRAY_NAME=adapter-bouncer POM_ARTIFACT_ID=adapter-bouncer POM_PACKAGING=aar -POM_VERSION=1.0.0 -CODE_VERSION=100 \ No newline at end of file +POM_VERSION=1.0.1 +CODE_VERSION=101 \ No newline at end of file diff --git a/vgscollect-bouncer/src/main/java/com/verygoodsecurity/api/bouncer/ScanActivity.kt b/vgscollect-bouncer/src/main/java/com/verygoodsecurity/api/bouncer/ScanActivity.kt index e22c149ba..525532394 100644 --- a/vgscollect-bouncer/src/main/java/com/verygoodsecurity/api/bouncer/ScanActivity.kt +++ b/vgscollect-bouncer/src/main/java/com/verygoodsecurity/api/bouncer/ScanActivity.kt @@ -67,11 +67,7 @@ class ScanActivity: BaseTransmitActivity(), CardScanActivityResultHandler { key, enableEnterCardManually, enableExpiryExtraction, - enableNameExtraction, - displayCardPan, - displayCardholderName, - displayCardScanLogo, - enableDebug + enableNameExtraction ) } @@ -170,22 +166,22 @@ class ScanActivity: BaseTransmitActivity(), CardScanActivityResultHandler { /** If true, display the card pan once the card has started to scan. */ - const val DISPLAY_CARD_PAN = "displayCardPan" + private const val DISPLAY_CARD_PAN = "displayCardPan" /** If true, display the name of the card owner if extracted. */ - const val DISPLAY_CARD_HOLDER_NAME = "displayCardholderName" + private const val DISPLAY_CARD_HOLDER_NAME = "displayCardholderName" /** If true, display the cardscan.io logo at the top of the screen. */ - const val DISPLAY_CARD_SCAN_LOGO = "displayCardScanLogo" + private const val DISPLAY_CARD_SCAN_LOGO = "displayCardScanLogo" /** If true, enable debug views in card scan. */ - const val ENABLE_DEBUG = "enableDebug" + private const val ENABLE_DEBUG = "enableDebug" /** Start the card scanner activity. From e85657b44ab54ef714ddaf43b7488a61a349e3f8 Mon Sep 17 00:00:00 2001 From: Dmytro Kos Date: Mon, 30 Nov 2020 19:41:45 +0300 Subject: [PATCH 13/13] fix lint; add RequiresApi for resize (#54) --- .../demoapp/activity_case/VGSCollectActivity.kt | 6 ++++-- .../core/storage/content/file/TemporaryFileStorage.kt | 3 +++ .../vgscollect/core/storage/content/file/VGSFileProvider.kt | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt b/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt index 2dd347772..30bf2266e 100644 --- a/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt +++ b/app/src/main/java/com/verygoodsecurity/demoapp/activity_case/VGSCollectActivity.kt @@ -223,8 +223,10 @@ class VGSCollectActivity: AppCompatActivity(), VgsCollectResponseListener, View. .setHostname("collect-android-testing.verygoodsecurity.io/test") .create() - val cacheSize = 10 * 1024 * 1024 // 10MB - vgsForm.getFileProvider().resize(cacheSize) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + val cacheSize = 10 * 1024 * 1024 // 10MB + vgsForm.getFileProvider().resize(cacheSize) + } } override fun onCreateOptionsMenu(menu: Menu): Boolean { diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt index 7fbedbb7a..ddcd32b72 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/TemporaryFileStorage.kt @@ -4,8 +4,10 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri +import android.os.Build import android.os.Bundle import android.util.LruCache +import androidx.annotation.RequiresApi import androidx.annotation.VisibleForTesting import com.verygoodsecurity.vgscollect.app.FilePickerActivity import com.verygoodsecurity.vgscollect.core.model.network.VGSError @@ -38,6 +40,7 @@ internal class TemporaryFileStorage( memoryCache.evictAll() } + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun resize(size: Int) { memoryCache.resize(size) } diff --git a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/VGSFileProvider.kt b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/VGSFileProvider.kt index 84658d7a7..a29250328 100644 --- a/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/VGSFileProvider.kt +++ b/vgscollect/src/main/java/com/verygoodsecurity/vgscollect/core/storage/content/file/VGSFileProvider.kt @@ -1,5 +1,7 @@ package com.verygoodsecurity.vgscollect.core.storage.content.file +import android.os.Build +import androidx.annotation.RequiresApi import com.verygoodsecurity.vgscollect.core.model.state.FileState /** @@ -13,6 +15,7 @@ interface VGSFileProvider { * * @param cacheSize The new maximum size. */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) fun resize(cacheSize: Int) /**