diff --git a/.github/workflows/ci-unitest-build.yml b/.github/workflows/ci-unitest-build.yml index 6af046d..d22c545 100644 --- a/.github/workflows/ci-unitest-build.yml +++ b/.github/workflows/ci-unitest-build.yml @@ -31,14 +31,27 @@ jobs: run: ./gradlew :flagship:clean - name: Build with Gradle run: ./gradlew :flagship:assembleDebug +# - name: Unit tests +# run: bash ./gradlew flagship:testJacocoUnitTestCoverage -i --stacktrace +# - name: Generate report +# run: bash ./gradlew flagship:createJacocoUnitTestCoverageReport - name: Unit tests - run: bash ./gradlew flagship:testJacocoUnitTestCoverage -i --stacktrace - - name: Generate report - run: bash ./gradlew flagship:createJacocoUnitTestCoverageReport + id: unit_tests + continue-on-error: true + run: bash ./gradlew flagship:jacocoDebugCodeCoverage -i --stacktrace + - name: Upload Test Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-report + path: flagship/build/reports/tests/testDebugUnitTest/ + - name: Stop if tests failed + if: steps.unit_tests.conclusion == 'failure' + run: exit 1 - name: Upload coverage to codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} - files: flagship/build/reports/coverage/test/jacoco/report.xml + files: flagship/build/reports/jacoco/jacocoDebugCodeCoverage/jacocoDebugCodeCoverage.xml - name: Build run: bash ./gradlew flagship:clean flagship:assembleRelease diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f263ff..9c511ba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,9 @@ jobs: run: ./gradlew :flagship:assembleDebug - name: Unit tests run: bash ./gradlew flagship:testJacocoUnitTestCoverage -i --stacktrace + - name: Get version + run: | + echo "FLAGSHIP_VERSION_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - name: Generate report run: bash ./gradlew flagship:createJacocoUnitTestCoverageReport - name: Upload coverage to codecov @@ -35,12 +38,12 @@ jobs: files: flagship/build/reports/coverage/test/jacoco/report.xml - name: Build and Publish env: - SONATYPE_SIGNING_KEY: ${{ secrets.SONATYPE_SIGNING_KEY }} - SONATYPE_SIGNING_PWD: ${{ secrets.SONATYPE_SIGNING_PWD }} - SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} - SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - SONATYPE_REPOSITORY: ${{ secrets.SONATYPE_REPOSITORY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGINMEMORYKEYID }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGINMEMORYKEYPASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME}} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGINMEMORYKEY }} run: | bash ./gradlew clean bash ./gradlew flagship:assembleRelease - bash ./gradlew publishToSonatype closeSonatypeStagingRepository + bash ./gradlew publishToMavenCentral --stacktrace diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index dc8182a..7ad2ed5 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -1,18 +1,37 @@ { - "version": 2, + "version": 3, "artifactType": { "type": "APK", "kind": "Directory" }, "applicationId": "com.abtasty.flagshipqa", - "variantName": "processReleaseResources", + "variantName": "release", "elements": [ { "type": "SINGLE", "filters": [], + "attributes": [], "versionCode": 1, "versionName": "1.0", "outputFile": "app-release.apk" } - ] + ], + "elementType": "File", + "baselineProfiles": [ + { + "minApi": 28, + "maxApi": 30, + "baselineProfiles": [ + "baselineProfiles/1/app-release.dm" + ] + }, + { + "minApi": 31, + "maxApi": 2147483647, + "baselineProfiles": [ + "baselineProfiles/0/app-release.dm" + ] + } + ], + "minSdkVersionForDexing": 21 } \ No newline at end of file diff --git a/app/src/main/java/com/abtasty/flagshipqa/MainActivity.kt b/app/src/main/java/com/abtasty/flagshipqa/MainActivity.kt index bc6aca0..06b6fed 100644 --- a/app/src/main/java/com/abtasty/flagshipqa/MainActivity.kt +++ b/app/src/main/java/com/abtasty/flagshipqa/MainActivity.kt @@ -1,25 +1,12 @@ package com.abtasty.flagshipqa -import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController -import com.abtasty.flagship.api.CacheStrategy -import com.abtasty.flagship.api.TrackingManagerConfig -import com.abtasty.flagship.hits.Screen -import com.abtasty.flagship.main.Flagship -import com.abtasty.flagship.main.FlagshipConfig import com.google.android.material.bottomnavigation.BottomNavigationView -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking class MainActivity : AppCompatActivity() { diff --git a/app/src/main/java/com/abtasty/flagshipqa/ui/dashboard/ConfigViewModel.kt b/app/src/main/java/com/abtasty/flagshipqa/ui/dashboard/ConfigViewModel.kt index daaf9ab..318b8a9 100644 --- a/app/src/main/java/com/abtasty/flagshipqa/ui/dashboard/ConfigViewModel.kt +++ b/app/src/main/java/com/abtasty/flagshipqa/ui/dashboard/ConfigViewModel.kt @@ -77,8 +77,9 @@ class ConfigViewModel(val appContext: Application) : AndroidViewModel(appContext error(errorStr) else { val flagshipConfig = if (useBucketing.value == true) FlagshipConfig.Bucketing() else FlagshipConfig.DecisionApi() - if (flagshipConfig is FlagshipConfig.Bucketing) + if (flagshipConfig is FlagshipConfig.Bucketing) { flagshipConfig.withPollingIntervals(pollingIntervalTime.value!!, getPollingIntervalUnit()) + } flagshipConfig.withTimeout(timeout.value ?: 2000) flagshipConfig.withLogLevel(LogManager.Level.ALL) flagshipConfig.withFlagshipStatusListener { status -> @@ -103,6 +104,7 @@ class ConfigViewModel(val appContext: Application) : AndroidViewModel(appContext TrackingManagerConfig( maxPoolSize = 5, batchTimeInterval = 10000 +// disablePolling = true ) ) // diff --git a/app/src/main/java/com/abtasty/flagshipqa/ui/modifications/ModificationViewModel.kt b/app/src/main/java/com/abtasty/flagshipqa/ui/modifications/ModificationViewModel.kt index d3a51b6..2d16641 100644 --- a/app/src/main/java/com/abtasty/flagshipqa/ui/modifications/ModificationViewModel.kt +++ b/app/src/main/java/com/abtasty/flagshipqa/ui/modifications/ModificationViewModel.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import com.abtasty.flagship.main.Flagship import com.abtasty.flagship.model.Flag -import com.abtasty.flagship.model.Modification import com.abtasty.flagship.model._Flag import com.abtasty.flagship.visitor.VisitorDelegate import org.json.JSONObject diff --git a/build.gradle b/build.gradle index 178745a..1e65417 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,5 @@ buildscript { - /* - ./gradlew publishToSonatype closeSonatypeStagingRepository - ./gradlew publishToSonatype -Dvariant=compat closeSonatypeStagingRepository - */ - ext { kotlin_version = '1.9.25' maven_artifact_id = "flagship-android" @@ -15,32 +10,26 @@ buildscript { if (!maven_variant.isEmpty()) { maven_artifact_id = maven_artifact_id + '-' + maven_variant } - flagship_version_name = System.getenv('FLAGSHIP_VERSION_NAME') ?: "4.0.0-beta1" - flagship_version_code = System.getenv('FLAGSHIP_VERSION_CODE') ?: 20 - sonatype_signing_key = System.getenv('SONATYPE_SIGNING_KEY') - sonatype_signing_pwd = System.getenv('SONATYPE_SIGNING_PWD') - sonatype_username = System.getenv('SONATYPE_USERNAME') ?: ossrhUsername - sonatype_password = System.getenv('SONATYPE_PASSWORD') ?: ossrhPassword - sonatype_repository_id = System.getenv('SONATYPE_REPOSITORY') ?: stagingRepositoryId + flagship_version_name = System.getenv('FLAGSHIP_VERSION_NAME') ?: "4.0.0-beta2" + flagship_version_code = System.getenv('FLAGSHIP_VERSION_CODE') ?: 21 } repositories { google() mavenCentral() - maven { url 'https://jitpack.io' } } dependencies { classpath 'com.android.tools.build:gradle:8.5.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.16" - classpath "io.github.gradle-nexus:publish-plugin:1.3.0" +// classpath "io.github.gradle-nexus:publish-plugin:1.3.0" } } plugins { - id 'com.google.devtools.ksp' version '1.9.10-1.0.13' apply false + id 'com.google.devtools.ksp' version '1.9.25-1.0.20' apply false id("io.github.gradle-nexus.publish-plugin") version "1.3.0" - id 'org.jetbrains.kotlin.android' version '1.9.20' apply false + id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false } allprojects { @@ -50,19 +39,7 @@ allprojects { } } -nexusPublishing { - repositories { - sonatype { - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) - stagingProfileId = sonatype_repository_id - username = sonatype_username - password = sonatype_password - } - } -} - -task clean(type: Delete) { - delete rootProject.buildDir +tasks.register('clean', Delete) { + delete rootProject.layout.buildDirectory } diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..d4e0df8 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,6 @@ +codecov: + require_ci_to_pass: yes + +ignore: + - "flagship/src/main/java/com/abtasty/flagship/database" + - "**/EAIWindowCallBack.kt" \ No newline at end of file diff --git a/flagship/build.gradle b/flagship/build.gradle index 1d3f203..a942461 100644 --- a/flagship/build.gradle +++ b/flagship/build.gradle @@ -1,19 +1,20 @@ +import com.vanniktech.maven.publish.SonatypeHost plugins { id 'com.android.library' id 'kotlin-android' id 'kotlin-parcelize' id 'com.google.devtools.ksp' + id 'com.vanniktech.maven.publish' version "0.30.0" + id 'jacoco' } -apply from: 'jacoco.gradle' - kotlin { jvmToolchain(11) } android { - publishNonDefault true + namespace 'com.abtasty.flagship' compileSdk 34 defaultConfig { @@ -30,8 +31,6 @@ android { unitTests.includeAndroidResources = true unitTests.returnDefaultValues = true - - unitTests { all { jvmArgs '-noverify' @@ -44,22 +43,20 @@ android { kotlinOptions { freeCompilerArgs = ["-Xdebug"] } - enableUnitTestCoverage true - enableAndroidTestCoverage true +// enableUnitTestCoverage true +// enableAndroidTestCoverage true + testCoverageEnabled true } release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } - all { + configureEach { buildConfigField "String", "FLAGSHIP_VERSION_NAME", "\"${flagship_version_name}\"" buildConfigField "int", "FLAGSHIP_VERSION_CODE", "${flagship_version_code}" resValue("integer", "ABTastyVersionCode", "${flagship_version_code}") resValue("string", "ABTastyVersion", "${flagship_version_name}") } - jacoco { - version = '0.8.11' - } } compileOptions { @@ -70,17 +67,15 @@ android { jvmTarget = '1.8' } - android.libraryVariants.all { variant -> + android.libraryVariants.configureEach { variant -> variant.outputs.all { output -> outputFileName = "flagship-android-${flagship_version_name}.aar" } } - namespace 'com.abtasty.flagship' -} -configurations { - common - compat + jacoco { + version = '0.8.12' + } } dependencies { @@ -94,9 +89,9 @@ dependencies { testImplementation 'androidx.test:core:1.6.1' testImplementation 'org.robolectric:robolectric:4.14.1' testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0" - implementation 'com.squareup.okhttp3:okhttp:4.9.3' + implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'androidx.lifecycle:lifecycle-process:2.8.7' annotationProcessor("androidx.room:room-compiler:2.6.1") @@ -109,4 +104,95 @@ repositories { mavenCentral() } -apply from: 'flagship-publishing.gradle' \ No newline at end of file + +mavenPublishing { + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) + + signAllPublications() + + coordinates(maven_group_id, maven_artifact_id, flagship_version_name) + + pom { + name = maven_artifact_id + description = 'Visit https://developers.flagship.io/ to get started with Flagship.' + url = 'https://github.com/flagship-io/flagship-android' + licenses { + license { + name = 'Apache License 2.0' + url = 'https://github.com/flagship-io/flagship-android/blob/master/LICENSE' + } + } + developers { + developer { + id = 'raf-abtasty' + name = 'Raphael' + email = 'raphael@abtasty.com' + } + } + scm { + connection = 'scm:git:github.com/flagship-io/flagship-android.git' + developerConnection = 'scm:git:ssh:github.com/flagship-io/flagship-android.git' + url = 'https://github.com/flagship-io/flagship-android/blob/master/' + } + } +} + +tasks.withType(Test).configureEach { + jacoco.includeNoLocationClasses = true + jacoco.excludes = ['jdk.internal.*'] +} + + +project.afterEvaluate { + android.libraryVariants.configureEach { variant -> + def variantName = variant.name + def unitTestTaskName = "test${variantName.capitalize()}UnitTest" + + + def exclusions = [ + "**/R.class", + "**/R\$*.class", + "**/BuildConfig.*", + "**/Manifest*.*", + "**/*Test*.*" + ] + + //gradle flagship:jacocoDebugCodeCoverage + tasks.register("jacoco${variantName.capitalize()}CodeCoverage", JacocoReport) { + dependsOn(["$unitTestTaskName"]) + + group = "Reporting" + description = "Execute UI and unit tests, generate and combine Jacoco coverage report" + + reports { + xml.required = true + html.required = true + } + + sourceDirectories.setFrom(files( + fileTree(layout.projectDirectory.dir("src/main/java/")) { + exclude(exclusions) + }.filter { file -> + true +// !file.name.contains("Dao") && +// !file.name.contains("_Impl") && +// !file.name.contains("DefaultDatabase") + } + ) + ) + classDirectories.setFrom(files( + fileTree(layout.buildDirectory.dir("tmp/kotlin-classes/")) { + exclude(exclusions) + }.filter { file -> + true +// !file.name.contains("Dao") && +// !file.name.contains("_Impl") && +// !file.name.contains("DefaultDatabase") + } + )) + executionData.setFrom(files( + fileTree(layout.buildDirectory) { include(["**/*.exec", "**/*.ec"]) } + )) + } + } +} diff --git a/flagship/flagship-publishing.gradle b/flagship/flagship-publishing.gradle index 5504cf9..dd94053 100644 --- a/flagship/flagship-publishing.gradle +++ b/flagship/flagship-publishing.gradle @@ -1,63 +1,3 @@ -apply plugin: 'signing' -apply plugin: 'maven-publish' - -project(':flagship') { - publishing { - publications { - maven(MavenPublication) { - groupId = maven_group_id - artifactId = maven_artifact_id - version = flagship_version_name - artifact "$buildDir/outputs/aar/" + maven_artifact_id + "-" + flagship_version_name + ".aar" - pom { - name = maven_artifact_id - description = 'Visit https://developers.flagship.io/ to get started with Flagship.' - url = 'https://github.com/flagship-io/flagship-android' - licenses { - license { - name = 'Apache License 2.0' - url = 'https://github.com/flagship-io/flagship-android/blob/master/LICENSE' - } - } - developers { - developer { - id = 'raf-abtasty' - name = 'Raphael' - email = 'raphael@abtasty.com' - } - } - scm { - connection = 'scm:git:github.com/flagship-io/flagship-android.git' - developerConnection = 'scm:git:ssh:github.com/flagship-io/flagship-android.git' - url = 'https://github.com/flagship-io/flagship-android/blob/master/' - } - } - pom.withXml { - def dependenciesNode = asNode().appendNode('dependencies') - configurations.implementation.allDependencies.each { dependency -> - if (dependency.name != "unspecified") { - def dependencyNode = dependenciesNode.appendNode('dependency') - dependencyNode.appendNode('groupId', dependency.group) - dependencyNode.appendNode('artifactId', dependency.name) - dependencyNode.appendNode('version', dependency.version) - } - } - } - } - } - - repositories { - maven { - name = "OSSRH" - url = 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/' - credentials { - username = sonatype_username - password = sonatype_password - } - } - } - } -} signing { if (sonatype_signing_key && sonatype_signing_pwd) { diff --git a/flagship/jacoco.exec b/flagship/jacoco.exec deleted file mode 100644 index 550410b..0000000 Binary files a/flagship/jacoco.exec and /dev/null differ diff --git a/flagship/jacoco.gradle b/flagship/jacoco.gradle index c6b3596..17c9b19 100644 --- a/flagship/jacoco.gradle +++ b/flagship/jacoco.gradle @@ -3,78 +3,85 @@ apply plugin: 'jacoco' tasks.withType(Test) { jacoco.includeNoLocationClasses = true - jacoco.excludes = ['jdk.internal.*'] + jacoco.excludes = ['jdk.internal.*', + 'com/abtasty/flagship/database/**' + ] maxParallelForks = 1 } project.afterEvaluate { - +// android.libraryVariants.all { variant -> def variantName = variant.name def testTaskName = "test${variantName.capitalize()}UnitTest" - +// tasks.create(name: "${testTaskName}Coverage", type: JacocoReport, dependsOn: "$testTaskName") { + println("#TASK -> " + "${testTaskName}Coverage") group = "Reporting" description = "Generate Jacoco coverage reports on the ${variantName.capitalize()} build." - +// reports { xml.required = true html.required = true html.outputLocation.set(layout.buildDirectory.dir("jacocoHtml")) } - def excludes = [ - '**/R.class', - '**/R$*.class', - '**/BuildConfig.*', - '**/Manifest*.*', - '**/*Test*.*', - 'android/**/*.*', - 'androidx/**/*.*' - ] - - def javaClasses = fileTree(dir: variant.javaCompiler.destinationDir, excludes: excludes) - .filter({ file -> - !file.name.contains('_Impl') && - !file.name.contains('Creator') && - !file.name.contains('MIGRATION') && - !file.name.contains('DefaultImpls') + classDirectories.setFrom( + files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + 'com/abtasty/flagship/database/**', // Exclude entire package +// 'com/example/service/Helper*' // Exclude specific pattern + ]) }) - def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}", excludes: excludes) - .filter({ file -> - !file.name.contains('_Impl') && - !file.name.contains('Creator') && - !file.name.contains('MIGRATION') && - !file.name.contains('DefaultImpls') - }) - - classDirectories.from = files([javaClasses], [kotlinClasses]) - - sourceDirectories.from = files([ - "$project.projectDir/src/main/java", -// "$project.projectDir/src/$variantBase/java", - ]) - - executionData.from = files([ - "${project.buildDir}/jacoco/${testTaskName}.exec" - ]) - } - } -} - -task testWithCoverage { - doLast { - exec { - workingDir rootProject.projectDir - commandLine 'sh', "./gradlew", ":flagship:clean" - } - exec { - workingDir rootProject.projectDir - commandLine 'sh', "./gradlew", ":flagship:assembleDebug" - } - exec { - workingDir rootProject.projectDir - commandLine 'sh', "./gradlew", ":flagship:testDebugUnitTestCoverage" + ) +//// def excludes = [ +//// '**/R.class', +//// '**/R$*.class', +//// '**/BuildConfig.*', +//// '**/Manifest*.*', +//// '**/*Test*.*', +//// 'android/**/*.*', +//// 'androidx/**/*.*' +//// ] +//// +//// def javaClasses = fileTree(dir: variant.javaCompiler.destinationDir, excludes: excludes) +//// .filter({ file -> +//// !file.name.contains('_Impl') && +//// !file.name.contains('Creator') && +//// !file.name.contains('MIGRATION') && +//// !file.name.contains('DefaultImpls') +//// }) +//// def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}", excludes: excludes) +//// .filter({ file -> +//// !file.name.contains('_Impl') && +//// !file.name.contains('Creator') && +//// !file.name.contains('MIGRATION') && +//// !file.name.contains('DefaultImpls') +//// }) +//// +//// +//// classDirectories.from = files([javaClasses], [kotlinClasses]) +//// +//// println("#P0 = " + variant.javaCompiler.destinationDir) +//// println("#P1 = " + "${buildDir}/tmp/kotlin-classes/${variantName}") +//// println("#P2 = " + "${project.buildDir}/jacoco/${testTaskName}.exec") +//// println("#P3 = " + "${project.projectDir}/jacoco/${testTaskName}.exec") +//// sourceDirectories.from = files([ +//// "$project.projectDir/src/main/java", +//// ]) +//// +////// executionData.from = files([ +////// "${project.buildDir}/jacoco/${testTaskName}.exec" +////// ]) +//// executionData.from = files([ +//// "${project.projectDir}/outputs/unit_test_code_coverage/jacocoUnitTest/${testTaskName}.exec" +//// ]) +// afterEvaluate { +// classDirectories = files(classDirectories.files.collect { +// fileTree(dir: it, +// exclude: ['']) +// }) +// } } } } \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/api/HttpManager.kt b/flagship/src/main/java/com/abtasty/flagship/api/HttpManager.kt index 0ddd537..dbbd4dc 100644 --- a/flagship/src/main/java/com/abtasty/flagship/api/HttpManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/api/HttpManager.kt @@ -34,7 +34,7 @@ import javax.net.ssl.X509TrustManager object HttpManager { - lateinit var client: OkHttpClient + var client: OkHttpClient? = null private var testOn = false private var threadPoolExecutor: ThreadPoolExecutor? = null private val workerTimeout = 500L @@ -50,6 +50,11 @@ object HttpManager { this.client = client } + internal fun clearClient() { + testOn = false + this.client = null + } + fun initHttpManager() { initThreadPoolExecutor() HttpCompat.insertProviderIfNeeded() @@ -99,28 +104,40 @@ object HttpManager { return trustManagerFactory } - private fun initHttpClient() : OkHttpClient { - if (!this::client.isInitialized || HttpCompat.clientInterceptors(client).isEmpty()) { - val newClientBuilder = OkHttpClient.Builder() - if (Build.VERSION.SDK_INT <= 25) { - val trustManagerFactory = getTrustManagerFactory() - val sslContext = SSLContext.getInstance("TLS") - sslContext.init(null, trustManagerFactory.trustManagers, null) - newClientBuilder.sslSocketFactory(sslContext.socketFactory, trustManagerFactory.trustManagers[0] as X509TrustManager) + private fun initHttpClient() { + try { + if (client == null || HttpCompat.clientInterceptors(client!!).isEmpty()) { + val newClientBuilder = OkHttpClient.Builder() + if (Build.VERSION.SDK_INT <= 25) { + val trustManagerFactory = getTrustManagerFactory() + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, trustManagerFactory.trustManagers, null) + newClientBuilder.sslSocketFactory( + sslContext.socketFactory, + trustManagerFactory.trustManagers[0] as X509TrustManager + ) + } + newClientBuilder.retryOnConnectionFailure(false) + newClientBuilder.dispatcher(Dispatcher(threadPoolExecutor as ExecutorService)) + newClientBuilder.callTimeout(Flagship.getConfig().timeout, TimeUnit.MILLISECONDS) + client = newClientBuilder.build() } - newClientBuilder.retryOnConnectionFailure(false) - newClientBuilder.dispatcher(Dispatcher(threadPoolExecutor as ExecutorService)) - newClientBuilder.callTimeout(Flagship.getConfig().timeout, TimeUnit.MILLISECONDS) - client = newClientBuilder.build() + } catch (e: Exception) { + FlagshipLogManager.exception(FlagshipConstants.Exceptions.Companion.FlagshipException(e)) } - return client } fun getThreadPoolExecutor(): ThreadPoolExecutor? { return threadPoolExecutor } - fun sendHttpRequest(type : RequestType , uri : String, headers : HashMap?, content : String?) : ResponseCompat { + fun sendHttpRequest( + type: RequestType, + uri: String, + headers: HashMap?, + content: String? + ): ResponseCompat? { + val builder = Request.Builder().url(uri) .addHeader("Content-Type", "application/json") System.getProperty("http.agent")?.let { @@ -135,10 +152,14 @@ object HttpManager { builder.post(body) } val request = builder.build() - val response = client.newCall(request).execute() - val responseCompat = ResponseCompat(response) - response.close() - return responseCompat + if (client == null) + initHttpManager() + client?.newCall(request)?.execute()?.let { response -> + val responseCompat = ResponseCompat(response) + response.close() + return responseCompat + } + return null } fun sendAsyncHttpRequest(type: RequestType, uri: String, headers: HashMap?, content: String?): Deferred { @@ -164,28 +185,6 @@ object HttpManager { headers["x-sdk-version"] = BuildConfig.FLAGSHIP_VERSION_NAME - val batch = JSONArray() - for (a in activateList) { - batch.put(a.data()) - } - val body = JSONObject() - body.put(FlagshipConstants.HitKeyMap.CLIENT_ID, Flagship.getConfig().envId) - body.put("batch", batch) - - return sendAsyncHttpRequest( - HttpManager.RequestType.POST, - IFlagshipEndpoints.DECISION_API + IFlagshipEndpoints.ACTIVATION, - null, - body.toString() - ) - } - - fun sendDeveloperUsageTrackingRequest(activateList: ArrayList>): Deferred { - val headers: HashMap = HashMap() - headers["x-sdk-client"] = "android" - headers["x-sdk-version"] = BuildConfig.FLAGSHIP_VERSION_NAME - - val batch = JSONArray() for (a in activateList) { batch.put(a.data()) diff --git a/flagship/src/main/java/com/abtasty/flagship/api/HttpResponseCompat.kt b/flagship/src/main/java/com/abtasty/flagship/api/HttpResponseCompat.kt index c8d43a7..9a24e64 100644 --- a/flagship/src/main/java/com/abtasty/flagship/api/HttpResponseCompat.kt +++ b/flagship/src/main/java/com/abtasty/flagship/api/HttpResponseCompat.kt @@ -20,11 +20,18 @@ abstract class HttpResponseCompat(protected var response: Response) { fun toJSON(): JSONObject { - val content = try { - JSONObject(content!!) - } catch (e: Exception) { - content - } +// val content = try { +// JSONObject(content!!) +// } catch (e: Exception) { +// content +// } + val contentAsJsonString = this.content?.let { content -> + try { + JSONObject(content) + } catch (e: Exception) { + content + } + } ?: content val jsonRequestHeaders = JSONObject() requestHeaders?.toMultimap()?.forEach { (k, v) -> jsonRequestHeaders.put(k, v.toString()) @@ -35,6 +42,6 @@ abstract class HttpResponseCompat(protected var response: Response) { .put("requestHeaders", jsonRequestHeaders) .put("requestBody", try { JSONObject(requestContent) } catch (e: Exception) { JSONObject()}) .put("responseHeaders", jsonResponseHeaders) - .put("responseBody", content) + .put("responseBody", contentAsJsonString ?: JSONObject.NULL) } } \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/api/TrackingManager.kt b/flagship/src/main/java/com/abtasty/flagship/api/TrackingManager.kt index 63aae15..d14de83 100644 --- a/flagship/src/main/java/com/abtasty/flagship/api/TrackingManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/api/TrackingManager.kt @@ -14,16 +14,19 @@ import com.abtasty.flagship.main.Flagship import com.abtasty.flagship.main.FlagshipConfig import com.abtasty.flagship.main.OnConfigChangedListener import com.abtasty.flagship.utils.FlagshipConstants +import com.abtasty.flagship.utils.FlagshipConstants.Exceptions.Companion.FlagshipException import com.abtasty.flagship.utils.FlagshipLogManager import com.abtasty.flagship.utils.LogManager import com.abtasty.flagship.utils.ResponseCompat import com.abtasty.flagship.utils.Utils +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withTimeout import org.json.JSONObject import java.util.concurrent.ConcurrentLinkedQueue @@ -31,15 +34,14 @@ import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit -import com.abtasty.flagship.utils.FlagshipConstants.Exceptions.Companion.FlagshipException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.GlobalScope /** * This class configure the Flagship SDK Tracking Manager which gathers all visitors emitted hits in a pool and * fire them in batch requests at regular time intervals withing a dedicated thread. */ + open class TrackingManagerConfig( + /** * Specifies the strategy to use for hits caching. It relies on the CacheManager provided in FlagshipConfig. * Default value is CacheStrategy.CONTINUOUS_CACHING. @@ -55,18 +57,23 @@ open class TrackingManagerConfig( /** * Specifies a time delay between each batched hit requests in milliseconds. Default value is 10000. */ - var batchTimeInterval: Long = 10000, + var batchTimeInterval: Long = TrackingManagerConfig.DEFAULT_BATCH_TIME_INTERVAL, /** * Specifies a max hit pool size that will trigger a batch request once reached. Default value is 10. */ - var maxPoolSize: Int = 10, + var maxPoolSize: Int = TrackingManagerConfig.DEFAULT_MAX_POOL_SIZE, /** * Disable polling and fire hits in separate requests one by one. */ open var disablePolling: Boolean = false -) {} +) { + companion object { + val DEFAULT_BATCH_TIME_INTERVAL: Long = 10000 + val DEFAULT_MAX_POOL_SIZE: Int = 10 + } +} interface TrackingManagerStrategyInterface { @@ -174,33 +181,44 @@ class TrackingManager() : OnConfigChangedListener, TrackingManagerStrategyInterf this.flagshipConfig = config this.trackingManagerConfig = config.trackingManagerConfig this.cacheManager = config.cacheManager - runBlocking(Dispatchers.Default) { - getStrategy().lookupPool().await() //new - } - if (this.trackingManagerConfig!!.disablePolling) { - this.clearPool() - } else { - this.startPollingLoop() + restartPollingIfNeeded() + } + + private fun restartPollingIfNeeded() { + Flagship.coroutineScope().async { + getStrategy().lookupPool().await() + startPollingLoop() } } internal fun startPollingLoop() { - - if (!running) { - executor ?: run { - executor = Executors.newSingleThreadScheduledExecutor { r -> - val t: Thread = Executors.defaultThreadFactory().newThread(r) - t.isDaemon = true - t + try { + if (this.trackingManagerConfig?.disablePolling == true) { + this.clearPool() + } else { + if (!running) { + executor ?: run { + executor = Executors.newSingleThreadScheduledExecutor { r -> + val t: Thread = Executors.defaultThreadFactory().newThread(r) + t.isDaemon = true + t + } + scheduledFuture = executor?.scheduleWithFixedDelay( + { + log(FlagshipConstants.Debug.TRACKING_MANAGER_POLLING) + getStrategy().polling() + }, + 0, + trackingManagerConfig?.batchTimeInterval + ?: TrackingManagerConfig.DEFAULT_BATCH_TIME_INTERVAL, + TimeUnit.MILLISECONDS + ) + this.running = true + } } - scheduledFuture = executor!!.scheduleWithFixedDelay( - { - log(FlagshipConstants.Debug.TRACKING_MANAGER_POLLING) - getStrategy().polling() - }, 0, trackingManagerConfig!!.batchTimeInterval, TimeUnit.MILLISECONDS - ) - this.running = true } + } catch (e: Exception) { + FlagshipLogManager.exception(FlagshipException(e)) } } @@ -247,17 +265,21 @@ class TrackingManager() : OnConfigChangedListener, TrackingManagerStrategyInterf } - private fun getStrategy(): AbstractCacheStrategy { + internal fun getStrategy(): AbstractCacheStrategy { return when (true) { (Flagship.getStatus() == Flagship.FlagshipStatus.PANIC) -> PanicStrategy(this) + (this.trackingManagerConfig?.disablePolling == true) -> NoPollingStrategy(this) + (this.trackingManagerConfig?.cachingStrategy == CacheStrategy.CONTINUOUS_CACHING) -> ContinuousCacheStrategy(this) + (this.trackingManagerConfig?.cachingStrategy == CacheStrategy.PERIODIC_CACHING) -> PeriodicCacheStrategy(this) + else -> ContinuousCacheStrategy(this) } } @@ -325,6 +347,7 @@ abstract class AbstractCacheStrategy(private val trackingManager: TrackingManage if (new) sendActivateBatch() } + is DeveloperUsageTracking -> { if (h is Usage || (h is TroubleShooting && Utils.isTroubleShootingEnabled())) { trackingManager.developerUsageTrackingQueue.add(h) @@ -332,6 +355,7 @@ abstract class AbstractCacheStrategy(private val trackingManager: TrackingManage sendDeveloperUsageTrackingHits() } } + else -> { trackingManager.hitQueue.add(h) TrackingManager.log( @@ -362,7 +386,8 @@ abstract class AbstractCacheStrategy(private val trackingManager: TrackingManage (if (h is Activate) trackingManager.activateQueue else trackingManager.hitQueue).remove(h) } if (hits.isNotEmpty()) { - TrackingManager.log(FlagshipConstants.Debug.TRACKING_MANAGER_REMOVED_HITS, + TrackingManager.log( + FlagshipConstants.Debug.TRACKING_MANAGER_REMOVED_HITS, hits.joinToString { it.id }) } hits @@ -466,7 +491,7 @@ abstract class AbstractCacheStrategy(private val trackingManager: TrackingManage } } catch (e: Exception) { e.printStackTrace() - CoroutineScope(Dispatchers.Default).async { null } + CoroutineScope(Dispatchers.IO).async { null } } } @@ -492,21 +517,28 @@ abstract class AbstractCacheStrategy(private val trackingManager: TrackingManage } } } - val response = HttpManager.sendAsyncHttpRequest( - HttpManager.RequestType.POST, - IFlagshipEndpoints.Companion.EVENTS, - null, - batch.data().toString() - ).await() - TrackingManager.logHitHttpResponse(response = response) - if (response == null || response.code !in 200..204) { - trackingManager.hitQueue.addAll(batch.hitList) - if (Utils.isTroubleShootingEnabled()) { - trackingManager.developerUsageTrackingQueue.add(TroubleShooting.Factory.SEND_BATCH_HIT_ROUTE_RESPONSE_ERROR.build(null, response)) - sendDeveloperUsageTrackingHits() + if (batch.hitList.size > 0) { + val response = HttpManager.sendAsyncHttpRequest( + HttpManager.RequestType.POST, + IFlagshipEndpoints.Companion.EVENTS, + null, + batch.data().toString() + ).await() + TrackingManager.logHitHttpResponse(response = response) + if (response == null || response.code !in 200..204) { + trackingManager.hitQueue.addAll(batch.hitList) + if (Utils.isTroubleShootingEnabled()) { + trackingManager.developerUsageTrackingQueue.add( + TroubleShooting.Factory.SEND_BATCH_HIT_ROUTE_RESPONSE_ERROR.build( + null, + response + ) + ) + sendDeveloperUsageTrackingHits() + } } - } - Pair(response, batch) + Pair(response, batch) + } else null } else null } } catch (e: Exception) { @@ -527,22 +559,29 @@ abstract class AbstractCacheStrategy(private val trackingManager: TrackingManage activateList.add(hit as Activate) } } - val response = HttpManager.sendActivatesRequest(activateList).await() - TrackingManager.logHitHttpResponse(response = response) - if (response == null || response.code !in 200..204) { - trackingManager.activateQueue.addAll(activateList) - if (Utils.isTroubleShootingEnabled()) { - trackingManager.developerUsageTrackingQueue.add(TroubleShooting.Factory.SEND_ACTIVATE_HIT_ROUTE_ERROR.build(null, response)) - sendDeveloperUsageTrackingHits() - } - } else { - for (a in activateList) { - trackingManager.flagshipConfig?.onVisitorExposed?.invoke( - a.exposedVisitor, a.exposedFlag - ) + if (activateList.size > 0) { + val response = HttpManager.sendActivatesRequest(activateList).await() + TrackingManager.logHitHttpResponse(response = response) + if (response == null || response.code !in 200..204) { + trackingManager.activateQueue.addAll(activateList) + if (Utils.isTroubleShootingEnabled()) { + trackingManager.developerUsageTrackingQueue.add( + TroubleShooting.Factory.SEND_ACTIVATE_HIT_ROUTE_ERROR.build( + null, + response + ) + ) + sendDeveloperUsageTrackingHits() + } + } else { + for (a in activateList) { + trackingManager.flagshipConfig?.onVisitorExposed?.invoke( + a.exposedVisitor, a.exposedFlag + ) + } } - } - Pair(response, activateList) + Pair(response, activateList) + } else null } else null } } catch (e: Exception) { @@ -768,14 +807,20 @@ class PeriodicCacheStrategy(val trackingManager: TrackingManager) : AbstractCach class NoPollingStrategy(val trackingManager: TrackingManager) : AbstractCacheStrategy(trackingManager) { + private val pollingMutex = Mutex() + override fun addHit(hit: Hit<*>, new: Boolean): Hit<*>? { return this.addHits(arrayListOf(hit), new)?.get(0) } override fun addHits(hits: ArrayList>, new: Boolean): ArrayList>? { val results = super.addHits(hits, new) - runBlocking { - polling().await() + Flagship.coroutineScope().launch { + kotlin.runCatching { + pollingMutex.withLock { + polling().await() + } + } } return results } @@ -801,13 +846,13 @@ class NoPollingStrategy(val trackingManager: TrackingManager) : AbstractCacheStr ensureActive() super.sendHitsBatch()?.await()?.also { (response, batch) -> if (response != null && response.code in 200..204) { - deleteHits(batch.hitList) + deleteHits(ArrayList(batch.hitList)) (trackingManager.cacheManager as? IHitCacheImplementation)?.flushHits( - batch.hitList.map { it.id } as ArrayList + ArrayList(batch.hitList).map { it.id } as ArrayList ) } else { (trackingManager.cacheManager as? IHitCacheImplementation)?.cacheHits( - HitCacheHelper.hitsToJSONCache(batch.hitList) + HitCacheHelper.hitsToJSONCache(ArrayList(batch.hitList)) ) } } @@ -906,4 +951,8 @@ class PanicStrategy(val trackingManager: TrackingManager) : AbstractCacheStrateg //Log disabled return null } + + override fun sendDeveloperUsageTrackingHits(): Deferred>>?>? { + return null + } } \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/cache/DefaultCacheManager.kt b/flagship/src/main/java/com/abtasty/flagship/cache/DefaultCacheManager.kt index d81e7f3..b72d8dc 100644 --- a/flagship/src/main/java/com/abtasty/flagship/cache/DefaultCacheManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/cache/DefaultCacheManager.kt @@ -23,7 +23,7 @@ class DefaultCacheManager() : CacheManager(), IVisitorCacheImplementation, IHitC private var db: DefaultDatabase? = null override fun openDatabase(envId: String) { - if (db == null || !db!!.isOpen) { + if (db == null || db?.isOpen == false) { db = Room.databaseBuilder(Flagship.application, DefaultDatabase::class.java, "flagship-$envId-cache.db") .addCallback(object : RoomDatabase.Callback() { override fun onOpen(db: SupportSQLiteDatabase) { diff --git a/flagship/src/main/java/com/abtasty/flagship/cache/VisitorCacheHelper.kt b/flagship/src/main/java/com/abtasty/flagship/cache/VisitorCacheHelper.kt index cbe6cac..90c7154 100644 --- a/flagship/src/main/java/com/abtasty/flagship/cache/VisitorCacheHelper.kt +++ b/flagship/src/main/java/com/abtasty/flagship/cache/VisitorCacheHelper.kt @@ -2,9 +2,7 @@ package com.abtasty.flagship.cache import com.abtasty.flagship.main.Flagship import com.abtasty.flagship.model.CampaignMetadata -import com.abtasty.flagship.model.Flag import com.abtasty.flagship.model.FlagMetadata -import com.abtasty.flagship.model.Modification import com.abtasty.flagship.model.VariationGroupMetadata import com.abtasty.flagship.model.VariationMetadata import com.abtasty.flagship.model._Flag diff --git a/flagship/src/main/java/com/abtasty/flagship/decision/ApiManager.kt b/flagship/src/main/java/com/abtasty/flagship/decision/ApiManager.kt index d5ed3fc..ee8f0b5 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/ApiManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/ApiManager.kt @@ -22,15 +22,17 @@ class ApiManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flagshipCo override fun init(listener: ((Flagship.FlagshipStatus) -> Unit)?) { super.init(listener) - sendAccountSettingsJsonRequest() //todo + sendAccountSettingsJsonRequest() readyLatch?.countDown() - if (getStatus().lessThan(Flagship.FlagshipStatus.INITIALIZED)) + if (getStatus().lessThan(Flagship.FlagshipStatus.INITIALIZED)) { statusListener?.invoke( if (panic) Flagship.FlagshipStatus.PANIC else Flagship.FlagshipStatus.INITIALIZED ) + } + initialized = true } // override fun parseTroubleShooting(json: JSONObject) { @@ -48,6 +50,7 @@ class ApiManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flagshipCo @Throws(IOException::class) private fun sendCampaignRequest(visitorDelegateDTO: VisitorDelegateDTO): ArrayList? { + var results : ArrayList? = null val json = JSONObject() val headers: HashMap = HashMap() headers["x-api-key"] = flagshipConfig.apiKey @@ -58,22 +61,23 @@ class ApiManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flagshipCo json.put("trigger_hit", false) json.put("visitor_consent", visitorDelegateDTO.hasConsented) json.put("context", visitorDelegateDTO.contextToJson()) - val response: ResponseCompat = HttpManager.sendHttpRequest(HttpManager.RequestType.POST, + HttpManager.sendHttpRequest(HttpManager.RequestType.POST, DECISION_API + flagshipConfig.envId + CAMPAIGNS, headers, json.toString() - ) - lastResponse = response - lastResponseTimestamp = System.currentTimeMillis() - logResponse(response) - val results = if (response.code < 400) { - parseCampaignsResponse(response.content) - } else { - sendTroubleshootingHit( - TroubleShooting.Factory.GET_CAMPAIGNS_ROUTE_RESPONSE_ERROR.build( - visitorDelegateDTO.visitorDelegate, - response + ) ?.let { response -> + lastResponse = response + lastResponseTimestamp = System.currentTimeMillis() + logResponse(response) + results = if (response.code < 400) { + parseCampaignsResponse(response.content) + } else { + sendTroubleshootingHit( + TroubleShooting.Factory.GET_CAMPAIGNS_ROUTE_RESPONSE_ERROR.build( + visitorDelegateDTO.visitorDelegate, + response + ) ) - ) - null + null + } } updateFlagshipStatus(if (panic) Flagship.FlagshipStatus.PANIC else Flagship.FlagshipStatus.INITIALIZED) return results @@ -109,7 +113,7 @@ class ApiManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flagshipCo null, null ) - response.let { + response?.let { lastResponse = response lastResponseTimestamp = System.currentTimeMillis() logResponse(response) @@ -135,5 +139,6 @@ class ApiManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flagshipCo } } - override fun stop() {} + override fun stop() { + } } diff --git a/flagship/src/main/java/com/abtasty/flagship/decision/BucketingManager.kt b/flagship/src/main/java/com/abtasty/flagship/decision/BucketingManager.kt index 278a251..0a1b634 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/BucketingManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/BucketingManager.kt @@ -35,6 +35,7 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag super.init(listener) if (getStatus().lessThan(Flagship.FlagshipStatus.INITIALIZED)) statusListener?.invoke(Flagship.FlagshipStatus.INITIALIZING) startPolling() + initialized = true } // override fun parseTroubleShooting(json: JSONObject) { @@ -62,10 +63,14 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag } val time: Long = flagshipConfig.pollingTime val unit: TimeUnit = flagshipConfig.pollingUnit - if (time == 0L) - executor!!.execute(runnable) - else - executor!!.scheduleWithFixedDelay(runnable, 0, time, unit) + try { + if (time == 0L) + executor!!.execute(runnable) + else + executor!!.scheduleWithFixedDelay(runnable, 0, time, unit) + } catch (e: Exception) { + FlagshipLogManager.exception(FlagshipConstants.Exceptions.Companion.FlagshipException(e)) + } } } diff --git a/flagship/src/main/java/com/abtasty/flagship/decision/DecisionManager.kt b/flagship/src/main/java/com/abtasty/flagship/decision/DecisionManager.kt index 3677092..fbaac8b 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/DecisionManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/DecisionManager.kt @@ -19,10 +19,10 @@ import java.util.concurrent.CountDownLatch abstract class DecisionManager(var flagshipConfig: FlagshipConfig<*>) : IDecisionManager { + internal var initialized = false internal var panic : Boolean = false internal var statusListener : ((Flagship.FlagshipStatus) -> Unit)? = null internal var readyLatch : CountDownLatch? = null - internal var lastResponseTimestamp = 0L internal var lastResponse : ResponseCompat? = null diff --git a/flagship/src/main/java/com/abtasty/flagship/decision/IDecisionManager.kt b/flagship/src/main/java/com/abtasty/flagship/decision/IDecisionManager.kt index 47a0e73..2a4e097 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/IDecisionManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/IDecisionManager.kt @@ -1,10 +1,7 @@ package com.abtasty.flagship.decision -import com.abtasty.flagship.model.Flag -import com.abtasty.flagship.model.Modification import com.abtasty.flagship.model._Flag import com.abtasty.flagship.visitor.VisitorDelegateDTO -import org.json.JSONObject interface IDecisionManager { fun getCampaignFlags(visitorDelegateDTO: VisitorDelegateDTO): HashMap? diff --git a/flagship/src/main/java/com/abtasty/flagship/eai/EAIGestureListener.kt b/flagship/src/main/java/com/abtasty/flagship/eai/EAIGestureListener.kt index 3e8e8fc..4aac29d 100644 --- a/flagship/src/main/java/com/abtasty/flagship/eai/EAIGestureListener.kt +++ b/flagship/src/main/java/com/abtasty/flagship/eai/EAIGestureListener.kt @@ -41,7 +41,7 @@ class EAIGestureListener( val y = abs(e.y - windowVisibleDisplayFrame.top).roundToInt() val time = e.eventTime.toString().takeLast(5) val duration = e.eventTime - e.downTime - log(message = "# DB click: $y $x") +// log(message = "# DB click: $y $x") onEAIEvents.onEAIClickEvent("$y,$x,$time,$duration;") return result } diff --git a/flagship/src/main/java/com/abtasty/flagship/eai/EAIManager.kt b/flagship/src/main/java/com/abtasty/flagship/eai/EAIManager.kt index ad8b77f..9331cfe 100644 --- a/flagship/src/main/java/com/abtasty/flagship/eai/EAIManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/eai/EAIManager.kt @@ -20,22 +20,21 @@ import com.abtasty.flagship.utils.FlagshipConstants.Exceptions.Companion.Flagshi import com.abtasty.flagship.utils.FlagshipLogManager import com.abtasty.flagship.utils.LogManager import com.abtasty.flagship.utils.Utils +import com.abtasty.flagship.utils.Utils.Companion.CompatScreenMetric.CancelableCountDownLatch import com.abtasty.flagship.visitor.VisitorDelegate import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.json.JSONObject import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -import com.abtasty.flagship.utils.Utils.Companion.CompatScreenMetric.CancelableCountDownLatch -import kotlinx.coroutines.Job class EAIManager() : OnEAIEvents { @@ -53,47 +52,62 @@ class EAIManager() : OnEAIEvents { scored: Boolean, segment: String? ) { - visitorDelegate.eaiScored = scored - if (segment != null && visitorDelegate.configManager.flagshipConfig.eaiActivationEnabled) { - visitorDelegate.eaiSegment = segment - visitorDelegate.getStrategy().updateContext("eai::eas", segment) + try { + visitorDelegate.eaiScored = scored + if (segment != null && visitorDelegate.configManager.flagshipConfig.eaiActivationEnabled) { + visitorDelegate.eaiSegment = segment + visitorDelegate.getStrategy().updateContext("eai::eas", segment) + } + visitorDelegate.getStrategy().cacheVisitor() + } catch (e: Exception) { + FlagshipLogManager.exception(FlagshipException(e)) } - visitorDelegate.getStrategy().cacheVisitor() } internal suspend fun pollEAISegment(visitor: VisitorDelegate): String? { - val response = HttpManager.sendAsyncHttpRequest( - HttpManager.RequestType.GET, - IFlagshipEndpoints.EAI_SERVING.format(Flagship.getConfig().envId, visitor.visitorId), - null, - null, - ).await() - TrackingManager.logHitHttpResponse(response = response) - if (response != null && response.code in 200..299 && response.content != null) { - try { - val responseContent = response.content - if (!responseContent.isNullOrEmpty()) { - val segment = JSONObject(responseContent).optJSONObject("eai")?.optString("eas") - if (!segment.isNullOrEmpty()) { - FlagshipLogManager.log( - FlagshipLogManager.Tag.EAI_SERVING, - LogManager.Level.DEBUG, - FlagshipConstants.Debug.EAI_GET_SEGMENT.format(visitor.visitorId, segment) - ) - Flagship.configManager.decisionManager?.sendTroubleshootingHit(TroubleShooting.Factory.EMOTIONS_AI_SCORE.build(visitor, response, segment)) - return segment + try { + val response = HttpManager.sendAsyncHttpRequest( + HttpManager.RequestType.GET, + IFlagshipEndpoints.EAI_SERVING.format(Flagship.getConfig().envId, visitor.visitorId), + null, + null, + ).await() + TrackingManager.logHitHttpResponse(response = response) + if (response != null && response.code in 200..299 && response.content != null) { + try { + val responseContent = response.content + if (!responseContent.isNullOrEmpty()) { + val segment = JSONObject(responseContent).optJSONObject("eai")?.optString("eas") + if (!segment.isNullOrEmpty()) { + FlagshipLogManager.log( + FlagshipLogManager.Tag.EAI_SERVING, + LogManager.Level.DEBUG, + FlagshipConstants.Debug.EAI_GET_SEGMENT.format(visitor.visitorId, segment) + ) + Flagship.configManager.decisionManager?.sendTroubleshootingHit( + TroubleShooting.Factory.EMOTIONS_AI_SCORE.build( + visitor, + response, + segment + ) + ) + return segment + } } + } catch (e: Exception) { + FlagshipLogManager.exception(FlagshipConstants.Exceptions.Companion.FlagshipException(e)) } - } catch (e: Exception) { - FlagshipLogManager.exception(FlagshipConstants.Exceptions.Companion.FlagshipException(e)) } - } - Flagship.configManager.decisionManager?.sendTroubleshootingHit( - TroubleShooting.Factory.EMOTION_AI_SCORING_FAILED.build( - visitor, - System.currentTimeMillis() + Flagship.configManager.decisionManager?.sendTroubleshootingHit( + TroubleShooting.Factory.EMOTION_AI_SCORING_FAILED.build( + visitor, + System.currentTimeMillis() + ) ) - ) + return null + } catch (e: Exception) { + FlagshipLogManager.exception(FlagshipException(e)) + } return null } } @@ -109,10 +123,8 @@ class EAIManager() : OnEAIEvents { private var windowMetricsObtained = false private var eaiCollectStartTimestamp: Long = 0L - private var eaiCollectLatch : CancelableCountDownLatch? = null + private var eaiCollectLatch: CancelableCountDownLatch? = null private var eaiCollectLastEventTimestamp: Long = 0 -// private var visitorId: String? = null -// private var anonymousId: String? = null private var visitorDelegate: VisitorDelegate? = null private var deviceSize = Size(0, 0) private var deviceDensity = 0f @@ -153,43 +165,54 @@ class EAIManager() : OnEAIEvents { fun init() { Flagship.application.registerActivityLifecycleCallbacks(activityLifecycleCallbacks) - activityGestureListener = EAIGestureListener(windowVisibleDisplayFrame, this) - runBlocking { + val gestureListener = EAIGestureListener(windowVisibleDisplayFrame, this) + activityGestureListener = gestureListener + runCatching { CoroutineScope(Job() + Dispatchers.Main).launch { - activityGestureDetector = GestureDetector(Flagship.application, activityGestureListener!!) + try { + activityGestureDetector = GestureDetector(Flagship.application, gestureListener) + } catch (e: Exception) { + FlagshipLogManager.exception(FlagshipException(e)) + } } } } suspend fun getActivityInfo(activity: Activity? = null) { - currentActivity = activity - resetActivityInfo() - if (activity != null) { - this.activityName = "${activity.packageName}${activity.localClassName}" - this.deviceDensity = activity.resources.displayMetrics.density - this.deviceSize = Utils.Companion.CompatScreenMetric.getDeviceDisplaySize(activity.windowManager) - this.windowVisibleDisplayFrame = measureWindowVisibleDisplayFrame(activity) - activityGestureListener?.updateWindowVisibleDisplayFrame(windowVisibleDisplayFrame) - if (eaiOnWindowDispatchTouchEvent == null) { - eaiOnWindowDispatchTouchEvent = object : OnWindowDispatchTouchEvent { - override fun onWindowDispatchTouchEvent(motionEvent: MotionEvent) { - if (eaiCollectStartTimestamp > 0) { - activityGestureListener?.onTouchEvent(motionEvent) - activityGestureDetector?.onTouchEvent(motionEvent) - motionEvent.recycle() + try { + currentActivity = activity + resetActivityInfo() + if (activity != null) { + this.activityName = "${activity.packageName}${activity.localClassName}" + this.deviceDensity = activity.resources.displayMetrics.density + this.deviceSize = Utils.Companion.CompatScreenMetric.getDeviceDisplaySize(activity.windowManager) + this.windowVisibleDisplayFrame = measureWindowVisibleDisplayFrame(activity) + activityGestureListener?.updateWindowVisibleDisplayFrame(windowVisibleDisplayFrame) + if (eaiOnWindowDispatchTouchEvent == null) { + eaiOnWindowDispatchTouchEvent = object : OnWindowDispatchTouchEvent { + override fun onWindowDispatchTouchEvent(motionEvent: MotionEvent) { + if (eaiCollectStartTimestamp > 0) { + activityGestureListener?.onTouchEvent(motionEvent) + activityGestureDetector?.onTouchEvent(motionEvent) + motionEvent.recycle() + } } } } - } - if (activity.window.callback !is EAIWindowCallback) { - activityWindowOriginalCallback = activity.window.callback - try { - activity.window.callback = - EAIWindowCallback(activityWindowOriginalCallback!!, eaiOnWindowDispatchTouchEvent!!) - } catch (e: Exception) { - e.printStackTrace() + if (activity.window.callback !is EAIWindowCallback) { + activityWindowOriginalCallback = activity.window.callback + try { + if (activityWindowOriginalCallback != null) { + activity.window.callback = + EAIWindowCallback(activityWindowOriginalCallback!!, eaiOnWindowDispatchTouchEvent!!) + } + } catch (e: Exception) { + FlagshipLogManager.exception(FlagshipException(e)) + } } } + } catch (e: Exception) { + FlagshipLogManager.exception(FlagshipException(e)) } } @@ -202,97 +225,97 @@ class EAIManager() : OnEAIEvents { var collectCoroutine: Deferred? = null internal fun startEAICollect(visitorDelegate: VisitorDelegate, activity: Activity? = null): Deferred { - eaiCollectLatch = CancelableCountDownLatch(1) - collectCoroutine = Flagship.coroutineScope().async { - try { - ensureActive() - if (!visitorDelegate.configManager.flagshipConfig.eaiCollectEnabled) { - log( - level = LogManager.Level.ERROR, - message = FlagshipConstants.Errors.EAI_COLLECT_DISABLED_ERROR - ) - return@async false - } else { - if (!visitorDelegate.eaiScored) { - Flagship.configManager.decisionManager?.sendTroubleshootingHit( - TroubleShooting.Factory.EMOTION_AI_START_COLLECTING.build( - visitorDelegate, - System.currentTimeMillis() - ) + eaiCollectLatch = CancelableCountDownLatch(1) + collectCoroutine = Flagship.coroutineScope().async { + try { + ensureActive() + if (!visitorDelegate.configManager.flagshipConfig.eaiCollectEnabled) { + log( + level = LogManager.Level.ERROR, + message = FlagshipConstants.Errors.EAI_COLLECT_DISABLED_ERROR + ) + return@async false + } else { + if (!visitorDelegate.eaiScored) { + Flagship.configManager.decisionManager?.sendTroubleshootingHit( + TroubleShooting.Factory.EMOTION_AI_START_COLLECTING.build( + visitorDelegate, + System.currentTimeMillis() ) - this@EAIManager.visitorDelegate = visitorDelegate - try { - if (activity != null) - getActivityInfo(activity) - } catch (e: Exception) { - e.printStackTrace() - } - sendEAIPageViewEvent() - val isTimeout = eaiCollectLatch?.await( - (EAI_COLLECT_SESSION_TIMEOUT).toLong(), - TimeUnit.MILLISECONDS - ) ?: false - if (eaiCollectLatch != null && !eaiCollectLatch!!.isCancelled() && isTimeout) { - if (visitorDelegate.configManager.flagshipConfig.eaiActivationEnabled) { - val segment = startEAISegmentPolling(visitorDelegate).await() - if (segment != null) { - cacheVisitorEAIStatus(visitorDelegate, true, segment) - log( - message = FlagshipConstants.Debug.EAI_COLLECT_SERVING_VISITOR_SUCCESS.format( - visitorDelegate.visitorId, - segment - ) - ) - Flagship.configManager.decisionManager?.sendTroubleshootingHit( - TroubleShooting.Factory.EMOTION_AI_STOP_COLLECTING.build( - visitorDelegate, - System.currentTimeMillis() - ) + ) + this@EAIManager.visitorDelegate = visitorDelegate + try { + if (activity != null) + getActivityInfo(activity) + } catch (e: Exception) { + e.printStackTrace() + } + sendEAIPageViewEvent() + val isEAICollectHasNotTimeout = eaiCollectLatch?.await( + (EAI_COLLECT_SESSION_TIMEOUT).toLong(), + TimeUnit.MILLISECONDS + ) ?: false + if (eaiCollectLatch != null && (eaiCollectLatch?.isCancelled() == false) && isEAICollectHasNotTimeout) { + if (visitorDelegate.configManager.flagshipConfig.eaiActivationEnabled) { + val segment = startEAISegmentPolling(visitorDelegate).await() + if (segment != null) { + cacheVisitorEAIStatus(visitorDelegate, true, segment) + log( + message = FlagshipConstants.Debug.EAI_COLLECT_SERVING_VISITOR_SUCCESS.format( + visitorDelegate.visitorId, + segment ) - return@async true - } else { - cacheVisitorEAIStatus(visitorDelegate, false, null) - log( - level = LogManager.Level.ERROR, - message = FlagshipConstants.Errors.EAI_COLLECT_SUCCESS_SERVING_FAIL_ERROR.format( - visitorDelegate.visitorId - ) + ) + Flagship.configManager.decisionManager?.sendTroubleshootingHit( + TroubleShooting.Factory.EMOTION_AI_STOP_COLLECTING.build( + visitorDelegate, + System.currentTimeMillis() ) - return@async false - } + ) + return@async true } else { + cacheVisitorEAIStatus(visitorDelegate, false, null) log( - message = FlagshipConstants.Debug.EAI_COLLECT_VISITOR_SUCCESS.format( + level = LogManager.Level.ERROR, + message = FlagshipConstants.Errors.EAI_COLLECT_SUCCESS_SERVING_FAIL_ERROR.format( visitorDelegate.visitorId ) ) - cacheVisitorEAIStatus(visitorDelegate, true, null) - return@async true + return@async false } } else { - val diffFromStartTimestamp = System.currentTimeMillis() - eaiCollectStartTimestamp log( - message = FlagshipConstants.Debug.EAI_COLLECT_VISITOR_STOPPED.format( - visitorDelegate.visitorId, diffFromStartTimestamp.toString() + message = FlagshipConstants.Debug.EAI_COLLECT_VISITOR_SUCCESS.format( + visitorDelegate.visitorId ) ) - return@async false + cacheVisitorEAIStatus(visitorDelegate, true, null) + return@async true } } else { + val diffFromStartTimestamp = System.currentTimeMillis() - eaiCollectStartTimestamp log( - message = FlagshipConstants.Debug.EAI_COLLECT_VISITOR_ALREADY_SCORED.format( - visitorDelegate.visitorId + message = FlagshipConstants.Debug.EAI_COLLECT_VISITOR_STOPPED.format( + visitorDelegate.visitorId, diffFromStartTimestamp.toString() ) ) - return@async true + return@async false } + } else { + log( + message = FlagshipConstants.Debug.EAI_COLLECT_VISITOR_ALREADY_SCORED.format( + visitorDelegate.visitorId + ) + ) + return@async true } - } catch (e: Exception) { - FlagshipLogManager.exception(FlagshipException(e)) - return@async false } + } catch (e: Exception) { + FlagshipLogManager.exception(FlagshipException(e)) + return@async false } - return collectCoroutine!! + } + return collectCoroutine ?: Flagship.coroutineScope().async { false } } fun checkEAIEventTimestamp(): Boolean { @@ -314,67 +337,75 @@ class EAIManager() : OnEAIEvents { ) { log(message = FlagshipConstants.Debug.EAI_COLLECT_LAST_EVENT.format(diffFromStartTimestamp.toString())) eaiCollectLastEventTimestamp = System.currentTimeMillis() - runBlocking { endCollect() } + Flagship.coroutineScope().launch { endCollect() } } } } fun sendEAIPageViewEvent() { - val page = Page("${activityNamePrefix}${activityName}") - .withFieldAndValue(FlagshipConstants.HitKeyMap.CLIENT_ID, Flagship.getConfig().envId) - .withVisitorIds(visitorDelegate?.visitorId!!, visitorDelegate?.anonymousId) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_ADD_BLOCK, false) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_BITS_PER_PIXEL, "24") - .withFieldAndValue( - FlagshipConstants.HitKeyMap.EAI_WINDOW_SIZE, - "${windowVisibleDisplayFrame.width()},${windowVisibleDisplayFrame.height()};" - ) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_TRACKING_PREFERENCE, "unknown") + visitorDelegate?.visitorId?.let { visitorId -> + + + val page = Page("${activityNamePrefix}${activityName}") + .withFieldAndValue(FlagshipConstants.HitKeyMap.CLIENT_ID, Flagship.getConfig().envId) + .withVisitorIds(visitorId, visitorDelegate?.anonymousId) + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_ADD_BLOCK, false) + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_BITS_PER_PIXEL, "24") + .withFieldAndValue( + FlagshipConstants.HitKeyMap.EAI_WINDOW_SIZE, + "${windowVisibleDisplayFrame.width()},${windowVisibleDisplayFrame.height()};" + ) + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_TRACKING_PREFERENCE, "unknown") // .withFieldAndValue( // FlagshipConstants.HitKeyMap.EAI_FONT, // File("/system/fonts").listFiles()?.map { it.name.replace(".ttf", "") } ?: "") - .withFieldAndValue( - FlagshipConstants.HitKeyMap.EAI_FONT, "[]" - ) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_FAKE_BROTHER_INFO, false) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_FAKE_LANGUAGE_INFO, false) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_FAKE_OS_INFO, false) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_FAKE_RESOLUTION_INFO, false) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_UL, getLocaleAsString()) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_EC, "eaiPageView") - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_DC, "android") - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_PXR, deviceDensity) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_PLU, "[]") - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_REFERER, "https://www.flagship.io/android-sdk/") - .withFieldAndValue( - FlagshipConstants.HitKeyMap.EAI_DISPLAY_SIZE, - "[${deviceSize.width},${deviceSize.height}]" - ) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_TOF, 120) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_TSP, "[0, false, false]") - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_UA, System.getProperty("http.agent") ?: "") - - Flagship.coroutineScope().launch { - val response = HttpManager.sendAsyncHttpRequest( - HttpManager.RequestType.POST, - IFlagshipEndpoints.EAI_COLLECT, - null, - page.data().toString() - ).await() - TrackingManager.logHitHttpResponse(response = response) - response?.let { - Flagship.configManager.decisionManager?.sendTroubleshootingHit( - TroubleShooting.Factory.EMOTION_AI_EVENT.build( - visitorDelegate, - page, - response - ) + .withFieldAndValue( + FlagshipConstants.HitKeyMap.EAI_FONT, "[]" ) - if (response?.code in 200..299) { - if (eaiCollectStartTimestamp == 0L) - this@EAIManager.eaiCollectStartTimestamp = System.currentTimeMillis() - log(message = FlagshipConstants.Debug.EAI_COLLECT_START_TIMESTAMP.format(eaiCollectStartTimestamp.toString())) - setEAILastEventTimestamp() + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_FAKE_BROTHER_INFO, false) + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_FAKE_LANGUAGE_INFO, false) + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_FAKE_OS_INFO, false) + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_FAKE_RESOLUTION_INFO, false) + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_UL, getLocaleAsString()) + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_EC, "eaiPageView") + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_DC, "android") + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_PXR, deviceDensity) + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_PLU, "[]") + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_REFERER, "https://www.flagship.io/android-sdk/") + .withFieldAndValue( + FlagshipConstants.HitKeyMap.EAI_DISPLAY_SIZE, + "[${deviceSize.width},${deviceSize.height}]" + ) + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_TOF, 120) + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_TSP, "[0, false, false]") + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_UA, System.getProperty("http.agent") ?: "") + + Flagship.coroutineScope().launch { + val response = HttpManager.sendAsyncHttpRequest( + HttpManager.RequestType.POST, + IFlagshipEndpoints.EAI_COLLECT, + null, + page.data().toString() + ).await() + TrackingManager.logHitHttpResponse(response = response) + response?.let { + Flagship.configManager.decisionManager?.sendTroubleshootingHit( + TroubleShooting.Factory.EMOTION_AI_EVENT.build( + visitorDelegate, + page, + response + ) + ) + if (response?.code in 200..299) { + if (eaiCollectStartTimestamp == 0L) + this@EAIManager.eaiCollectStartTimestamp = System.currentTimeMillis() + log( + message = FlagshipConstants.Debug.EAI_COLLECT_START_TIMESTAMP.format( + eaiCollectStartTimestamp.toString() + ) + ) + setEAILastEventTimestamp() + } } } } @@ -387,136 +418,117 @@ class EAIManager() : OnEAIEvents { private suspend fun measureWindowVisibleDisplayFrame(activity: Activity): Rect { - val latch = CountDownLatch(1) val rect = Rect() - activity.window.decorView.post { - activity.window.decorView.getWindowVisibleDisplayFrame(rect) - if (!windowMetricsObtained && windowVisibleDisplayFrame.width() > 0 - && !(intArrayOf( - rect.top, - rect.left, - rect.right, - rect.bottom - ).none { it == 0 }) - ) { - latch.countDown() + try { + val latch = CountDownLatch(1) + activity.window.decorView.post { + try { + activity.window.decorView.getWindowVisibleDisplayFrame(rect) + if (!windowMetricsObtained && windowVisibleDisplayFrame.width() > 0 + && !(intArrayOf( + rect.top, + rect.left, + rect.right, + rect.bottom + ).none { it == 0 }) + ) { + latch.countDown() + } + } catch (e: Exception) { + FlagshipLogManager.exception(FlagshipException(e)) + } } + withContext(Dispatchers.IO) { + latch.await(200, TimeUnit.MILLISECONDS) + } + windowMetricsObtained = true + } catch (e: Exception) { + FlagshipLogManager.exception(FlagshipException(e)) } - withContext(Dispatchers.IO) { - latch.await(200, TimeUnit.MILLISECONDS) - } - windowMetricsObtained = true return rect } override fun onEAIClickEvent(click: String) { - if (checkEAIEventTimestamp()) { - val hit = VisitorEvent("${activityNamePrefix}${activityName}") - .withFieldAndValue(FlagshipConstants.HitKeyMap.CLIENT_ID, Flagship.getConfig().envId) - .withVisitorIds(visitorDelegate?.visitorId!!, visitorDelegate?.anonymousId) - .withFieldAndValue( - FlagshipConstants.HitKeyMap.EAI_WINDOW_SIZE, - "${windowVisibleDisplayFrame.width()},${windowVisibleDisplayFrame.height()};" - ) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_CLICK, click) //click positions - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_SCROLL, "") - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_MOVE, "") - Flagship.flagshipCoroutineScope.launch { - val response = HttpManager.sendAsyncHttpRequest( - HttpManager.RequestType.POST, - IFlagshipEndpoints.EAI_COLLECT, - null, - hit.data().toString() - ).await() - TrackingManager.logHitHttpResponse(response = response) - response?.let { - if (response.code in 200..299) - setEAILastEventTimestamp() - Flagship.configManager.decisionManager?.sendTroubleshootingHit( - TroubleShooting.Factory.EMOTION_AI_EVENT.build( - visitorDelegate, - hit, - response - ) + visitorDelegate?.visitorId?.let { visitorId -> + if (checkEAIEventTimestamp()) { + val hit = VisitorEvent("${activityNamePrefix}${activityName}") + .withFieldAndValue(FlagshipConstants.HitKeyMap.CLIENT_ID, Flagship.getConfig().envId) + .withVisitorIds(visitorId, visitorDelegate?.anonymousId) + .withFieldAndValue( + FlagshipConstants.HitKeyMap.EAI_WINDOW_SIZE, + "${windowVisibleDisplayFrame.width()},${windowVisibleDisplayFrame.height()};" ) + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_CLICK, click) //click positions + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_SCROLL, "") + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_MOVE, "") + Flagship.flagshipCoroutineScope.launch { + val response = HttpManager.sendAsyncHttpRequest( + HttpManager.RequestType.POST, + IFlagshipEndpoints.EAI_COLLECT, + null, + hit.data().toString() + ).await() + TrackingManager.logHitHttpResponse(response = response) + response?.let { + if (response.code in 200..299) + setEAILastEventTimestamp() + Flagship.configManager.decisionManager?.sendTroubleshootingHit( + TroubleShooting.Factory.EMOTION_AI_EVENT.build( + visitorDelegate, + hit, + response + ) + ) + } } } } } override fun onEAIScrollEvent(scroll: String, moves: String?) { - if (checkEAIEventTimestamp()) { - val hit = VisitorEvent("${activityNamePrefix}${activityName}") - .withFieldAndValue(FlagshipConstants.HitKeyMap.CLIENT_ID, Flagship.getConfig().envId) - .withVisitorIds(visitorDelegate?.visitorId!!, visitorDelegate?.anonymousId) - .withFieldAndValue( - FlagshipConstants.HitKeyMap.EAI_WINDOW_SIZE, - "${windowVisibleDisplayFrame.width()},${windowVisibleDisplayFrame.height()};" - ) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_SCROLL, scroll) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_MOVE, moves ?: "") - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_CLICK, "") - Flagship.flagshipCoroutineScope.launch { - val response = HttpManager.sendAsyncHttpRequest( - HttpManager.RequestType.POST, - IFlagshipEndpoints.EAI_COLLECT, - null, - hit.data().toString() - ).await() - TrackingManager.logHitHttpResponse(response = response) - response?.let { - if (response.code in 200..299) - setEAILastEventTimestamp() - Flagship.configManager.decisionManager?.sendTroubleshootingHit( - TroubleShooting.Factory.EMOTION_AI_EVENT.build(visitorDelegate, - hit, - response - ) + visitorDelegate?.visitorId?.let { visitorId -> + if (checkEAIEventTimestamp()) { + val hit = VisitorEvent("${activityNamePrefix}${activityName}") + .withFieldAndValue(FlagshipConstants.HitKeyMap.CLIENT_ID, Flagship.getConfig().envId) + .withVisitorIds(visitorId, visitorDelegate?.anonymousId) + .withFieldAndValue( + FlagshipConstants.HitKeyMap.EAI_WINDOW_SIZE, + "${windowVisibleDisplayFrame.width()},${windowVisibleDisplayFrame.height()};" ) + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_SCROLL, scroll) + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_MOVE, moves ?: "") + .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_CLICK, "") + + Flagship.flagshipCoroutineScope.launch { + val response = HttpManager.sendAsyncHttpRequest( + HttpManager.RequestType.POST, + IFlagshipEndpoints.EAI_COLLECT, + null, + hit.data().toString() + ).await() + TrackingManager.logHitHttpResponse(response = response) + response?.let { + if (response.code in 200..299) + setEAILastEventTimestamp() + Flagship.configManager.decisionManager?.sendTroubleshootingHit( + TroubleShooting.Factory.EMOTION_AI_EVENT.build( + visitorDelegate, + hit, + response + ) + ) + } } } } } override fun onEAIMoveEvent(moves: String) { - if (checkEAIEventTimestamp()) { - val hit = VisitorEvent("${activityNamePrefix}${activityName}") - .withFieldAndValue(FlagshipConstants.HitKeyMap.CLIENT_ID, Flagship.getConfig().envId) - .withVisitorIds(visitorDelegate?.visitorId!!, visitorDelegate?.anonymousId) - .withFieldAndValue( - FlagshipConstants.HitKeyMap.EAI_WINDOW_SIZE, - "${windowVisibleDisplayFrame.width()},${windowVisibleDisplayFrame.height()};" - ) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_MOVE, moves) - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_SCROLL, "") - .withFieldAndValue(FlagshipConstants.HitKeyMap.EAI_CLICK, "") - - Flagship.flagshipCoroutineScope.launch { - val response = HttpManager.sendAsyncHttpRequest( - HttpManager.RequestType.POST, - IFlagshipEndpoints.EAI_COLLECT, - null, - hit.data().toString() - ).await() - TrackingManager.logHitHttpResponse(response = response) - response?.let { - if (response.code in 200..299) - setEAILastEventTimestamp() - Flagship.configManager.decisionManager?.sendTroubleshootingHit( - TroubleShooting.Factory.EMOTION_AI_EVENT.build( - visitorDelegate, - hit, - response - ) - ) - } - } - } } suspend fun startEAISegmentPolling(visitor: VisitorDelegate): Deferred { - return CoroutineScope(Dispatchers.Default).async { + return CoroutineScope(Dispatchers.IO).async { try { Flagship.configManager.decisionManager?.sendTroubleshootingHit( TroubleShooting.Factory.EMOTION_AI_START_SCORING.build( @@ -543,24 +555,26 @@ class EAIManager() : OnEAIEvents { } suspend fun endCollect() { - Flagship.application.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks) - activityName = null - if (activityWindowOriginalCallback != null) - currentActivity?.window?.callback = activityWindowOriginalCallback - activityWindowOriginalCallback = null - eaiOnWindowDispatchTouchEvent = null - currentActivity = null - activityGestureListener = null - activityGestureDetector = null - windowVisibleDisplayFrame = Rect() - windowMetricsObtained = false - eaiCollectStartTimestamp = 0L - eaiCollectLastEventTimestamp = 0 - visitorDelegate = null + runCatching { + Flagship.application.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks) + activityName = null + if (activityWindowOriginalCallback != null) + currentActivity?.window?.callback = activityWindowOriginalCallback + activityWindowOriginalCallback = null + eaiOnWindowDispatchTouchEvent = null + currentActivity = null + activityGestureListener = null + activityGestureDetector = null + windowVisibleDisplayFrame = Rect() + windowMetricsObtained = false + eaiCollectStartTimestamp = 0L + eaiCollectLastEventTimestamp = 0 + visitorDelegate = null // visitorId = null - deviceSize = Size(0, 0) - deviceDensity = 0f - eaiCollectLatch?.countDown() + deviceSize = Size(0, 0) + deviceDensity = 0f + eaiCollectLatch?.countDown() + } } fun log( @@ -572,28 +586,29 @@ class EAIManager() : OnEAIEvents { } suspend fun onStop() { - Flagship.application.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks) - activityName = null - eaiPollingSegmentCoroutine?.cancelAndJoin() - if (activityWindowOriginalCallback != null) - currentActivity?.window?.callback = activityWindowOriginalCallback - activityWindowOriginalCallback = null - eaiOnWindowDispatchTouchEvent = null - currentActivity = null - activityGestureListener = null - activityGestureDetector = null - windowVisibleDisplayFrame = Rect() - windowMetricsObtained = false - eaiCollectStartTimestamp = 0L - eaiCollectLatch?.countDown() - eaiCollectLatch = null - eaiCollectLastEventTimestamp = 0 - visitorDelegate = null -// visitorId = null - deviceSize = Size(0, 0) - deviceDensity = 0f - if (collectCoroutine?.isActive == true) - collectCoroutine?.cancelAndJoin() - collectCoroutine = null + kotlin.runCatching { + Flagship.application.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks) + activityName = null + eaiPollingSegmentCoroutine?.cancelAndJoin() + if (activityWindowOriginalCallback != null) + currentActivity?.window?.callback = activityWindowOriginalCallback + activityWindowOriginalCallback = null + eaiOnWindowDispatchTouchEvent = null + currentActivity = null + activityGestureListener = null + activityGestureDetector = null + windowVisibleDisplayFrame = Rect() + windowMetricsObtained = false + eaiCollectStartTimestamp = 0L + eaiCollectLatch?.countDown() + eaiCollectLatch = null + eaiCollectLastEventTimestamp = 0 + visitorDelegate = null + deviceSize = Size(0, 0) + deviceDensity = 0f + if (collectCoroutine?.isActive == true) + collectCoroutine?.cancelAndJoin() + collectCoroutine = null + } } } \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/hits/Activate.kt b/flagship/src/main/java/com/abtasty/flagship/hits/Activate.kt index db23c2f..4814254 100644 --- a/flagship/src/main/java/com/abtasty/flagship/hits/Activate.kt +++ b/flagship/src/main/java/com/abtasty/flagship/hits/Activate.kt @@ -1,20 +1,22 @@ package com.abtasty.flagship.hits import com.abtasty.flagship.model.ExposedFlag +import com.abtasty.flagship.model.FlagMetadata import com.abtasty.flagship.utils.FlagshipConstants import com.abtasty.flagship.visitor.VisitorExposed import org.json.JSONObject +import java.util.HashMap /** * Internal Hit for activations */ -class Activate: Hit { +class Activate : Hit { - lateinit var exposedVisitor: VisitorExposed - lateinit var exposedFlag: ExposedFlag<*> + var exposedVisitor: VisitorExposed = VisitorExposed("", null, hashMapOf(), false, false) + var exposedFlag: ExposedFlag<*> = ExposedFlag("", "", "", FlagMetadata.EmptyFlagMetadata()) - constructor(exposedVisitor: VisitorExposed, exposedFlag: ExposedFlag<*>): super(Hit.Companion.Type.ACTIVATION) { + constructor(exposedVisitor: VisitorExposed, exposedFlag: ExposedFlag<*>) : super(Hit.Companion.Type.ACTIVATION) { this.withVisitorIds(exposedVisitor.visitorId, exposedVisitor.anonymousId) this.exposedVisitor = exposedVisitor this.exposedFlag = exposedFlag @@ -38,14 +40,25 @@ class Activate: Hit { return this } - internal constructor(jsonObject: JSONObject): super(Hit.Companion.Type.ACTIVATION, jsonObject) { - exposedVisitor = VisitorExposed.fromCacheJSON(jsonObject.getJSONObject("exposedVisitor"))!! - exposedFlag = ExposedFlag.fromCacheJSON(jsonObject.getJSONObject("exposedFlag"))!! + internal constructor(jsonObject: JSONObject) : super(Hit.Companion.Type.ACTIVATION, jsonObject) { + exposedVisitor = VisitorExposed.fromCacheJSON(jsonObject.getJSONObject("exposedVisitor")) ?: VisitorExposed( + "", + null, + hashMapOf(), + false, + false + ) + exposedFlag = ExposedFlag.fromCacheJSON(jsonObject.getJSONObject("exposedFlag")) ?: ExposedFlag( + "", + "", + "", + FlagMetadata.EmptyFlagMetadata() + ) } override fun checkHitValidity(): Boolean { - return when(true) { + return when (true) { (!checkTimestampValidity()) -> false (!checkSizeValidity()) -> false this.data.isNull(FlagshipConstants.HitKeyMap.VISITOR_ID) -> false diff --git a/flagship/src/main/java/com/abtasty/flagship/hits/Batch.kt b/flagship/src/main/java/com/abtasty/flagship/hits/Batch.kt index 043f7f5..adeb068 100644 --- a/flagship/src/main/java/com/abtasty/flagship/hits/Batch.kt +++ b/flagship/src/main/java/com/abtasty/flagship/hits/Batch.kt @@ -14,12 +14,10 @@ class Batch: Hit { this.data.put(FlagshipConstants.HitKeyMap.HIT_BATCH, JSONArray()) } - internal constructor(jsonObject: JSONObject): super(Companion.Type.BATCH, jsonObject) { - - } + internal constructor(jsonObject: JSONObject) : super(Companion.Type.BATCH, jsonObject) {} fun addChild(childHit: Hit<*>): Boolean { - if (childHit.checkHitValidity() && checkSizeValidity(childHit.size())) { + if (checkHitValidity() && childHit.checkHitValidity() && checkSizeValidity(childHit.size())) { this.hitList.add(childHit) this.data.getJSONArray(FlagshipConstants.HitKeyMap.HIT_BATCH).put(childHit.data) return true diff --git a/flagship/src/main/java/com/abtasty/flagship/hits/Segment.kt b/flagship/src/main/java/com/abtasty/flagship/hits/Segment.kt index f897bee..3603ba5 100644 --- a/flagship/src/main/java/com/abtasty/flagship/hits/Segment.kt +++ b/flagship/src/main/java/com/abtasty/flagship/hits/Segment.kt @@ -12,7 +12,8 @@ internal class Segment: Hit { obj.put(c.key, c.value) } this.data.put(FlagshipConstants.HitKeyMap.VISITOR_ID, visitorId) - this.data.put(FlagshipConstants.HitKeyMap.SEGMENT_LIST, obj) + if (obj.length() > 0) + this.data.put(FlagshipConstants.HitKeyMap.SEGMENT_LIST, obj) } internal constructor(jsonObject: JSONObject): super(Companion.Type.SEGMENT, jsonObject) @@ -20,7 +21,7 @@ internal class Segment: Hit { override fun checkHitValidity(): Boolean { return when(true) { (!super.checkHitValidity()) -> false - (this.data.isNull(FlagshipConstants.HitKeyMap.SEGMENT_LIST)) -> true + (this.data.isNull(FlagshipConstants.HitKeyMap.SEGMENT_LIST)) -> false else -> true } } diff --git a/flagship/src/main/java/com/abtasty/flagship/main/ConfigManager.kt b/flagship/src/main/java/com/abtasty/flagship/main/ConfigManager.kt index 709f289..f44fa0b 100644 --- a/flagship/src/main/java/com/abtasty/flagship/main/ConfigManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/main/ConfigManager.kt @@ -13,7 +13,12 @@ import com.abtasty.flagship.decision.DecisionManager import com.abtasty.flagship.eai.EAIManager import com.abtasty.flagship.main.Flagship.DecisionMode import com.abtasty.flagship.main.FlagshipConfig.DecisionApi -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch + interface OnConfigChangedListener { fun onConfigChanged(config: FlagshipConfig<*>) @@ -69,31 +74,41 @@ class ConfigManager : DefaultLifecycleObserver { } suspend fun stop() { - decisionManager?.stop() - decisionManager = null - flagshipConfig = DecisionApi() - trackingManager?.stop()?.await() - trackingManager = null - cacheManager.closeDatabase() - eaiManager?.onStop() + kotlin.runCatching { + decisionManager?.stop() + decisionManager = null + flagshipConfig = DecisionApi() + trackingManager?.stop()?.await() + trackingManager = null + cacheManager.closeDatabase() + eaiManager?.onStop() + } } override fun onStart(owner: LifecycleOwner) { trackingManager?.startPollingLoop() - (decisionManager as? BucketingManager)?.startPolling() + if (decisionManager?.initialized == true) { + (decisionManager as? BucketingManager)?.startPolling() + } super.onStart(owner) } override fun onStop(owner: LifecycleOwner) { trackingManager?.stopPollingLoop() - (decisionManager as? BucketingManager)?.stop() + if (decisionManager?.initialized == true) + (decisionManager as? BucketingManager)?.stop() super.onStop(owner) } override fun onDestroy(owner: LifecycleOwner) { - runBlocking { + CoroutineScope(SupervisorJob() + Dispatchers.Default).launch { + ensureActive() stop() } super.onDestroy(owner) } + + internal fun bindToLifeCycle(lifecycleOwner: LifecycleOwner) { + lifecycleOwner.lifecycle.addObserver(this) + } } diff --git a/flagship/src/main/java/com/abtasty/flagship/main/Flagship.kt b/flagship/src/main/java/com/abtasty/flagship/main/Flagship.kt index 24fe87e..ea24f0e 100644 --- a/flagship/src/main/java/com/abtasty/flagship/main/Flagship.kt +++ b/flagship/src/main/java/com/abtasty/flagship/main/Flagship.kt @@ -108,7 +108,7 @@ object Flagship { instanceId = UUID.randomUUID().toString() initializationTimeStamp = System.currentTimeMillis() supervisorJob = SupervisorJob() - flagshipCoroutineScope = CoroutineScope(supervisorJob + Dispatchers.Default) + flagshipCoroutineScope = CoroutineScope(supervisorJob + Dispatchers.IO) val handler = Handler(Looper.getMainLooper()) handler.post { ProcessLifecycleOwner.get().lifecycle.addObserver(configManager) diff --git a/flagship/src/main/java/com/abtasty/flagship/model/Campaign.kt b/flagship/src/main/java/com/abtasty/flagship/model/Campaign.kt index f652954..c02e16d 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/Campaign.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/Campaign.kt @@ -68,6 +68,11 @@ data class Campaign(val campaignMetadata: CampaignMetadata, val variationGroups: } override fun toString(): String { - return "Campaign(id='${campaignMetadata.campaignId}', variationGroups=$variationGroups)" + return "Campaign(campaignMetadata=$campaignMetadata, variationGroups=$variationGroups)" } + +// override fun toString(): String { +// return "Campaign(id='${campaignMetadata.campaignId}', variationGroups=$variationGroups)" +// } + } \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/model/CampaignMetadata.kt b/flagship/src/main/java/com/abtasty/flagship/model/CampaignMetadata.kt index a5e6450..94182ff 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/CampaignMetadata.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/CampaignMetadata.kt @@ -27,4 +27,10 @@ open class CampaignMetadata( campaignMetadata.campaignType, campaignMetadata.slug ) + + override fun toString(): String { + return "CampaignMetadata(campaignId='$campaignId', campaignName='$campaignName', campaignType='$campaignType', slug='$slug')" + } + + } \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/model/Modification.kt b/flagship/src/main/java/com/abtasty/flagship/model/Modification.kt deleted file mode 100644 index 570a1ad..0000000 --- a/flagship/src/main/java/com/abtasty/flagship/model/Modification.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.abtasty.flagship.model - -data class Modification( - val key: String, val campaignId: String, val variationGroupId: String, - val variationId: String, val isReference: Boolean, val value: Any?, val campaignType: String, - val slug: String -) { - - override fun toString(): String { - return "Modification(key='$key', campaignId='$campaignId', variationGroupId='$variationGroupId'," + - " variationId='$variationId', isReference=$isReference, value=$value, slug=$slug)" - } -} \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/model/Modifications.kt b/flagship/src/main/java/com/abtasty/flagship/model/Modifications.kt deleted file mode 100644 index 1578b1e..0000000 --- a/flagship/src/main/java/com/abtasty/flagship/model/Modifications.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.abtasty.flagship.model - -import com.abtasty.flagship.utils.FlagshipConstants -import com.abtasty.flagship.utils.FlagshipLogManager -import com.abtasty.flagship.utils.LogManager -import org.json.JSONArray -import org.json.JSONObject - -data class Modifications( - val campaignId: String, val variationGroupId: String, val variationId: String, - val isReference: Boolean, val type: String, - val values: HashMap = HashMap() -) { - - companion object { - - fun parse(campaignId: String, campaignType : String, slug: String, variationGroupId: String, variationId: String, isReference: Boolean, modificationsObj: JSONObject): Modifications? { - return try { - val type = modificationsObj.getString("type") - val values: HashMap = HashMap() - val valueObj = modificationsObj.getJSONObject("value") - for (key in valueObj.keys()) { - val value = if (valueObj.isNull(key)) null else valueObj[key] - if (value is Boolean || value is Number || value is String || value is JSONObject || value is JSONArray || value == null) - values[key] = Modification(key, campaignId, variationGroupId, variationId, isReference, value, campaignType, slug) - else - FlagshipLogManager.log(FlagshipLogManager.Tag.PARSING, LogManager.Level.ERROR, FlagshipConstants.Errors.PARSING_MODIFICATION_ERROR + " _ _ 3 _ _ ") - } - Modifications(campaignId, variationGroupId, variationId, isReference, type, values) - } catch (e: Exception) { - FlagshipLogManager.log(FlagshipLogManager.Tag.PARSING, LogManager.Level.ERROR, FlagshipConstants.Errors.PARSING_MODIFICATION_ERROR + " _ _ 4 _ _ ") - null - } - } - } - - override fun toString(): String { - return "Modifications(campaignId='$campaignId', variationGroupId='$variationGroupId', " + - "variationId='$variationId', isReference=$isReference, type='$type', values=$values)" - } -} diff --git a/flagship/src/main/java/com/abtasty/flagship/utils/HttpCompat.kt b/flagship/src/main/java/com/abtasty/flagship/utils/HttpCompat.kt index c7c3f1b..a03cd2b 100644 --- a/flagship/src/main/java/com/abtasty/flagship/utils/HttpCompat.kt +++ b/flagship/src/main/java/com/abtasty/flagship/utils/HttpCompat.kt @@ -14,8 +14,8 @@ class HttpCompat { return request.url.toString() } - fun clientInterceptors(client: OkHttpClient): List { - return client.interceptors + fun clientInterceptors(client: OkHttpClient?): List { + return client?.interceptors ?: arrayListOf() } fun requestJson(request: Request) : JSONObject { diff --git a/flagship/src/main/java/com/abtasty/flagship/visitor/DefaultStrategy.kt b/flagship/src/main/java/com/abtasty/flagship/visitor/DefaultStrategy.kt index 2e3f953..fb2a799 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/DefaultStrategy.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/DefaultStrategy.kt @@ -6,7 +6,6 @@ import com.abtasty.flagship.cache.IVisitorCacheImplementation import com.abtasty.flagship.cache.VisitorCacheHelper import com.abtasty.flagship.decision.DecisionManager import com.abtasty.flagship.eai.EAIManager -import com.abtasty.flagship.eai.EAIManager.Companion.pollEAISegment import com.abtasty.flagship.hits.Activate import com.abtasty.flagship.hits.Consent import com.abtasty.flagship.hits.Hit @@ -102,7 +101,6 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) return Flagship.coroutineScope().async { visitor.updateFlagsStatus(FlagStatus.FETCHING, FetchFlagsRequiredStatusReason.NONE) ensureActive() - if (decisionManager?.flagshipConfig?.eaiActivationEnabled == true) { if (!visitor.eaiScored) visitor.eaiSegment = EAIManager.pollEAISegment(visitor) @@ -345,7 +343,7 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) override fun collectEmotionsAIEvents(activity: Activity?): Deferred { return if (Flagship.configManager.eaiManager != null) - Flagship.configManager.eaiManager!!.startEAICollect(visitor, activity) + Flagship.configManager.eaiManager?.startEAICollect(visitor, activity) ?: Flagship.coroutineScope().async { false } else Flagship.coroutineScope().async { false } } diff --git a/flagship/src/main/java/com/abtasty/flagship/visitor/NoConsentStrategy.kt b/flagship/src/main/java/com/abtasty/flagship/visitor/NoConsentStrategy.kt index 737b4e4..d466136 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/NoConsentStrategy.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/NoConsentStrategy.kt @@ -24,7 +24,7 @@ class NoConsentStrategy(val visitorDelegate: VisitorDelegate) : DefaultStrategy( // Call default getModificationValue private fun logMethodDeactivatedError(tag: FlagshipLogManager.Tag?, visitorId: String?, methodName: String?) { - FlagshipLogManager.log(tag!!, LogManager.Level.ERROR, String.format(FlagshipConstants.Errors.METHOD_DEACTIVATED_CONSENT_ERROR, methodName, visitorId)) + FlagshipLogManager.log(tag ?: FlagshipLogManager.Tag.VISITOR, LogManager.Level.ERROR, String.format(FlagshipConstants.Errors.METHOD_DEACTIVATED_CONSENT_ERROR, methodName, visitorId)) } override fun sendVisitorExposition(key: String, defaultValue : T?, valueConsumedTimestamp: Long) { diff --git a/flagship/src/main/java/com/abtasty/flagship/visitor/NotReadyStrategy.kt b/flagship/src/main/java/com/abtasty/flagship/visitor/NotReadyStrategy.kt index fa1d185..de86b47 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/NotReadyStrategy.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/NotReadyStrategy.kt @@ -5,14 +5,9 @@ import com.abtasty.flagship.hits.Hit import com.abtasty.flagship.hits.TroubleShooting import com.abtasty.flagship.main.Flagship import com.abtasty.flagship.model.FlagMetadata -import com.abtasty.flagship.model.Modification -import com.abtasty.flagship.model._Flag -import com.abtasty.flagship.utils.FlagshipConstants import com.abtasty.flagship.utils.FlagshipLogManager -import com.abtasty.flagship.utils.LogManager import kotlinx.coroutines.Deferred import kotlinx.coroutines.async -import org.json.JSONObject class NotReadyStrategy(val visitorDelegate: VisitorDelegate) : DefaultStrategy(visitorDelegate) { diff --git a/flagship/src/main/java/com/abtasty/flagship/visitor/PanicStrategy.kt b/flagship/src/main/java/com/abtasty/flagship/visitor/PanicStrategy.kt index 1d15b58..4f183bf 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/PanicStrategy.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/PanicStrategy.kt @@ -4,13 +4,10 @@ import android.app.Activity import com.abtasty.flagship.hits.Hit import com.abtasty.flagship.main.Flagship import com.abtasty.flagship.model.FlagMetadata -import com.abtasty.flagship.model.Modification -import com.abtasty.flagship.model._Flag import com.abtasty.flagship.utils.FlagshipContext import com.abtasty.flagship.utils.FlagshipLogManager import kotlinx.coroutines.Deferred import kotlinx.coroutines.async -import org.json.JSONObject class PanicStrategy(val visitorDelegate: VisitorDelegate) : DefaultStrategy(visitorDelegate) { diff --git a/flagship/src/main/java/com/abtasty/flagship/visitor/Visitor.kt b/flagship/src/main/java/com/abtasty/flagship/visitor/Visitor.kt index 440c953..1cd890f 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/Visitor.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/Visitor.kt @@ -132,7 +132,7 @@ class Visitor(internal val configManager: ConfigManager, visitorId: String, isAu this@Visitor } } catch (e: Exception) { - return CoroutineScope(Job() + Dispatchers.Default).async { this@Visitor } + return CoroutineScope(Job() + Dispatchers.IO).async { this@Visitor } } } diff --git a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegate.kt b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegate.kt index a3fd22e..afccd98 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegate.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegate.kt @@ -25,7 +25,7 @@ class VisitorDelegate( var onFlagStatusChanged: OnFlagStatusChanged? = null ) { - lateinit var sessionId: String + var sessionId: String = UUID.randomUUID().toString() var visitorId: String var anonymousId: String? = null var visitorContext: ConcurrentMap = ConcurrentHashMap() @@ -41,7 +41,6 @@ class VisitorDelegate( internal var eaiSegment: String? = null init { - sessionId = UUID.randomUUID().toString() updateFlagsStatus(FlagStatus.FETCH_REQUIRED, FetchFlagsRequiredStatusReason.FLAGS_NEVER_FETCHED) this.visitorId = if (visitorId == null || visitorId.isEmpty()) generateUUID() else visitorId this.isAuthenticated = isAuthenticated @@ -65,7 +64,7 @@ class VisitorDelegate( internal fun logVisitor(tag: FlagshipLogManager.Tag?) { val visitorStr = String.format(FlagshipConstants.Errors.VISITOR, visitorId, this) - FlagshipLogManager.log(tag!!, LogManager.Level.DEBUG, visitorStr) + FlagshipLogManager.log(tag ?: FlagshipLogManager.Tag.VISITOR, LogManager.Level.DEBUG, visitorStr) } internal fun loadContext(newContext: HashMap?) { diff --git a/flagship/src/test/assets/account_settings_panic.json b/flagship/src/test/assets/account_settings_panic.json new file mode 100644 index 0000000..b1274ad --- /dev/null +++ b/flagship/src/test/assets/account_settings_panic.json @@ -0,0 +1,5 @@ +{ + "panic": true, + "accountSettings": { + } +} \ No newline at end of file diff --git a/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsBlocks.kt b/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsBlocks.kt new file mode 100644 index 0000000..788ce99 --- /dev/null +++ b/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsBlocks.kt @@ -0,0 +1,643 @@ +package com.abtasty.flagship + +import android.graphics.Rect +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.abtasty.flagship.AFlagshipTest.Companion.ACCOUNT_SETTINGS +import com.abtasty.flagship.AFlagshipTest.Companion.ACTIVATION_URL +import com.abtasty.flagship.AFlagshipTest.Companion.ARIANE_URL +import com.abtasty.flagship.AFlagshipTest.Companion.CAMPAIGNS_URL +import com.abtasty.flagship.AFlagshipTest.Companion._API_KEY_ +import com.abtasty.flagship.AFlagshipTest.Companion._ENV_ID_ +import com.abtasty.flagship.AFlagshipTest.Companion.clientOverridden +import com.abtasty.flagship.AFlagshipTest.Companion.getApplication +import com.abtasty.flagship.api.ContinuousCacheStrategy +import com.abtasty.flagship.api.HttpManager +import com.abtasty.flagship.api.PanicStrategy +import com.abtasty.flagship.cache.HitCacheHelper +import com.abtasty.flagship.hits.Batch +import com.abtasty.flagship.hits.DeveloperUsageTracking +import com.abtasty.flagship.hits.Event +import com.abtasty.flagship.hits.Item +import com.abtasty.flagship.hits.Page +import com.abtasty.flagship.hits.Screen +import com.abtasty.flagship.hits.Segment +import com.abtasty.flagship.hits.TroubleShooting +import com.abtasty.flagship.hits.VisitorEvent +import com.abtasty.flagship.main.Flagship +import com.abtasty.flagship.main.FlagshipConfig +import com.abtasty.flagship.model.Campaign +import com.abtasty.flagship.model.CampaignMetadata +import com.abtasty.flagship.model.Targeting +import com.abtasty.flagship.model.TargetingList +import com.abtasty.flagship.model.Variation +import com.abtasty.flagship.model.VariationGroupMetadata +import com.abtasty.flagship.model.VariationMetadata +import com.abtasty.flagship.utils.ETargetingComp +import com.abtasty.flagship.utils.FetchFlagsRequiredStatusReason +import com.abtasty.flagship.utils.FlagStatus +import com.abtasty.flagship.utils.FlagshipConstants +import com.abtasty.flagship.utils.FlagshipContext +import com.abtasty.flagship.utils.HttpCompat +import com.abtasty.flagship.utils.LogManager +import com.abtasty.flagship.utils.MurmurHash.Companion.getAllocationFromMurmur +import com.abtasty.flagship.utils.OnFlagStatusChanged +import com.abtasty.flagship.visitor.NotReadyStrategy +import com.abtasty.flagship.visitor.Visitor +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.json.JSONArray +import org.json.JSONObject +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.android.controller.ActivityController +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import org.robolectric.shadows.ShadowLog +import java.util.LinkedList +import kotlin.math.log + + +@RunWith(RobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST) +@Config(application = CustomApplication::class, sdk = [24], qualifiers = "fr-rFR-w360dp-h640dp-xhdpi") +class FlagshipTestsBlocks { + + @Before + fun before() { + System.setProperty( + "http.agent", + "Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.200 Mobile Safari/537.36" + ); + ShadowLog.stream = System.out + if (!clientOverridden) { + AFlagshipTest.overrideClient() + clientOverridden = true + } + FlagshipTestsHelper.interceptor().clear() + } + + @After + fun after() { + runBlocking { + FlagshipTestsHelper.interceptor().clear() + clientOverridden = false + HttpManager.clearClient() + delay(200) + } + } + + @Test + fun test_block_campaign() { + assertTrue(Campaign.parse(JSONObject("{}")) == null) + assertTrue(Campaign.parse(JSONArray("[]"))?.size == 0) + val c = Campaign(CampaignMetadata("id", "name", "type", "slug"), LinkedList()) + assertTrue(c.toString().contains("name")) + } + + @Test + fun test_block_flag() { + FlagshipTestsHelper.interceptor() + .intercept( + CAMPAIGNS_URL.format(_ENV_ID_), + FlagshipTestsHelper.responseFromAssets(getApplication(), "api_response_1.json", 200) + ).intercept( + ARIANE_URL, + FlagshipTestsHelper.response("", 500) + ).intercept( + ACTIVATION_URL, + FlagshipTestsHelper.response("", 200) + ) + runBlocking { + Flagship.start( + RuntimeEnvironment.getApplication(), _ENV_ID_, _API_KEY_, FlagshipConfig.DecisionApi() + + ).await() + } + val visitor = Flagship.newVisitor("isVIPUser", true) + .build() + runBlocking { + visitor.fetchFlags().await() + } + val flag = visitor.getFlag("featureEnabled") + assertFalse(flag.value(true)!!) + assertTrue(flag.toString().contains("featureEnabled")) + } + + @Test + fun test_block_targeting_list() { + assertTrue(TargetingList.parse(JSONObject("{}")) == null) + val targetingList = TargetingList.parse(JSONObject("{\n" + + " \"targetings\": [\n" + + " {\n" + + " }\n" + + " ]\n" + + " }")) + assertTrue(targetingList!!.isTargetingValid(hashMapOf())) + assertTrue(TargetingList(null).isTargetingValid(hashMapOf())) + } + + @Test + fun test_block_targeting() { + assertTrue(Targeting.parse(JSONObject("{}")) == null) + val targeting = Targeting.parse( + JSONObject( + "{\n" + + " \"operator\": \"EQUALS\",\n" + + " \"key\": \"isVIPUser\",\n" + + " \"value\": true\n" + + " }" + ) + ) + assertTrue(targeting?.isTargetingValid(hashMapOf("isVIPUser" to true))!!) + assertFalse(targeting?.isTargetingValid(hashMapOf("isVIPUser" to false))!!) + assertFalse(targeting?.isTargetingValid(hashMapOf("bool" to false))!!) + val targeting2 = Targeting.parse( + JSONObject( + "{\n" + + " \"operator\": \"EQUALS\",\n" + + " \"key\": \"fs_all_users\",\n" + + " \"value\": true\n" + + " }" + ) + ) + assertTrue(targeting2?.isTargetingValid(hashMapOf("bool" to false))!!) + val targeting3 = Targeting.parse( + JSONObject( + "{\n" + + " \"operator\": \"unknown\",\n" + + " \"key\": \"fs_all_users\",\n" + + " \"value\": true\n" + + " }" + ) + ) + assertFalse(targeting3?.isTargetingValid(hashMapOf("bool" to false))!!) + } + + @Test + fun test_block_variations() { + val v = Variation.parse(JSONObject("{}"), false, VariationGroupMetadata("", "", CampaignMetadata())) + val v2 = Variation.parse( + JSONObject( + "{\n" + + " \"id\": \"bu6lgeu3bdt01555555\",\n" + + " \"modifications\": {\n" + + " \"type\": \"JSON\",\n" + + " \"value\": {\n" + + " \"target\": \"is\"\n" + + " }\n" + + " },\n" + + " \"reference\": false\n" + + " }" + ), false, VariationGroupMetadata("", "", CampaignMetadata()) + ) + assertTrue(v == null) + assertTrue(v2.toString().isNotEmpty()) + val flags = Variation.parse_flags( + JSONObject("{}"), + VariationMetadata("", "", false, 100, VariationGroupMetadata("", "", CampaignMetadata())) + ) + assertTrue(flags == null) + } + + @Test + fun test_block_init_http_client() { + runBlocking { + HttpManager.clearClient() + Flagship.start(RuntimeEnvironment.getApplication(), _ENV_ID_, _API_KEY_, FlagshipConfig.DecisionApi()) + .await() + } + assert(Flagship.getStatus() == Flagship.FlagshipStatus.INITIALIZED) + HttpManager.initHttpManager() + } + + @Test + fun test_block_tracking_manager_panic_strategy() { + + FlagshipTestsHelper.interceptor() + .intercept( + ACCOUNT_SETTINGS.format(_ENV_ID_), + FlagshipTestsHelper.responseFromAssets(getApplication(), "account_settings_panic.json", 200) + ) + + runBlocking { + Flagship.start(RuntimeEnvironment.getApplication(), _ENV_ID_, _API_KEY_, FlagshipConfig.DecisionApi()) + .await() + } + assertEquals(Flagship.FlagshipStatus.PANIC, Flagship.getStatus()) + val trackingManager = Flagship.configManager.trackingManager +// val panicStrategy = Flagship.configManager.trackingManager?.getStrategy()!! + assertTrue(trackingManager!!.getStrategy() is PanicStrategy) + val hit = Screen("test").withVisitorIds("vid_test", null) + assertTrue(trackingManager.addHit(hit) == null) + assertTrue(trackingManager.addHits(arrayListOf(hit)) == null) + assertTrue(trackingManager.deleteHits(arrayListOf(hit)) == null) + assertTrue(trackingManager.deleteHitsByVisitorId("vid_test") == null) + runBlocking { + assertTrue(trackingManager.lookupPool().await() == null) + assertFalse(trackingManager.cachePool().await()) + assertTrue(trackingManager.polling().await() == null) + } + assertTrue(trackingManager.sendHitsBatch() == null) + assertTrue(trackingManager.sendActivateBatch() == null) + assertTrue(trackingManager.sendDeveloperUsageTrackingHits() == null) + } + + + @Test + fun test_block_tracking_manager_continuous_strategy() { + runBlocking { + Flagship.start(RuntimeEnvironment.getApplication(), _ENV_ID_, _API_KEY_, FlagshipConfig.DecisionApi()) + .await() + } + assert(Flagship.getStatus() == Flagship.FlagshipStatus.INITIALIZED) + val strategy = Flagship.configManager.trackingManager?.getStrategy()!! + assertTrue(strategy is ContinuousCacheStrategy) + val hit = Screen("test").withVisitorIds("vid_test", null) + assertEquals(hit, strategy.addHit(hit)) + assertEquals(hit, strategy.deleteHits(arrayListOf(hit))!![0]) + } + + @Test + fun test_cache_helper() { + val hitCacheHelper = HitCacheHelper() + val json12 = HitCacheHelper.HitMigrations.MIGRATION_1_2.migrate(JSONObject("{\"time\":10}")) + assertTrue(json12.getLong("id") > 0) + val json23 = HitCacheHelper.HitMigrations.MIGRATION_2_3.migrate(json12) + assertFalse(json23.has("time")) + assertEquals(10, json23.getLong("timestamp")) + assertEquals(json12.getLong("id").toString(), json23.getString("id")) + + assertEquals(null, HitCacheHelper.HitMigrations.apply(json23)) + } + + @Test + fun test_hit_batch() { + val batch = Batch() + val screen = Screen("test_batch_hit") + screen.withVisitorIds("vid", null) + batch.withVisitorIds("vid", null) + assertTrue(batch.addChild(screen)) + assertEquals(1, batch.length()) + assertFalse(batch.addChild(Page("invalid_url"))) + assertEquals(1, batch.length()) + val json = HitCacheHelper.hitsToJSONCache(arrayListOf(batch)) + } + + @Test + fun test_hit_visitor_event() { + val invalidVisitorEvent = VisitorEvent("invalid_url") + val validVisitorEvent = VisitorEvent("https://valid_url.io") + assertFalse(invalidVisitorEvent.checkHitValidity()) + assertTrue(validVisitorEvent.checkHitValidity()) + } + + @Test + fun test_hit_page() { + assertFalse(Page("invalid").checkHitValidity()) + assertFalse(Page("").checkHitValidity()) + val p = Page("https://page.com") + p.timestamp = 1 + assertFalse(p.checkHitValidity()) + val page = Page("https://page.com") + .withVisitorIds("vid", null) + assertTrue(page.checkHitValidity()) + Page(page.toCacheJSON().getJSONObject("data")) + } + + @Test + fun test_hit_item() { + assertFalse(Item("", "1", "1").checkHitValidity()) + assertFalse(Item("1", "", "").checkHitValidity()) + assertFalse(Item("1", "1", "").checkHitValidity()) + val p = Item("1", "1", "1") + p.timestamp = 1 + assertFalse(p.checkHitValidity()) + val p2 = Item("1", "1", "1") + .withVisitorIds("vid", null) + assertTrue(p2.checkHitValidity()) + Item(p2.toCacheJSON().getJSONObject("data")) + } + + @Test + fun test_hit_troubleshooting() { + runBlocking { + Flagship.start(RuntimeEnvironment.getApplication(), _ENV_ID_, _API_KEY_, FlagshipConfig.DecisionApi()) + .await() + } + val visitor = Flagship.newVisitor("vid", true).build() + assertTrue(TroubleShooting.Factory.ACCOUNT_SETTINGS.build(visitor.delegate) == null) + assertTrue(TroubleShooting.Factory.VISITOR_SEND_HIT.build(visitor.delegate) == null) + assertTrue(TroubleShooting.Factory.SEND_BATCH_HIT_ROUTE_RESPONSE_ERROR.build(visitor.delegate) == null) + assertTrue(TroubleShooting.Factory.EXPOSURE_FLAG_BEFORE_CALLING_VALUE_METHOD.build(visitor.delegate) == null) + assertTrue(TroubleShooting.Factory.GET_CAMPAIGNS_ROUTE_RESPONSE_ERROR.build(visitor.delegate) == null) + assertTrue(TroubleShooting.Factory.EMOTION_AI_EVENT.build(visitor.delegate) == null) + assertTrue(TroubleShooting.Factory.EMOTION_AI_SCORING_FAILED.build(visitor.delegate) == null) + assertTrue(TroubleShooting.Factory.EMOTION_AI_START_COLLECTING.build(visitor.delegate) == null) + assertTrue(TroubleShooting.Factory.ERROR_CATCHED.build(visitor.delegate) == null) + assertTrue(TroubleShooting.Factory.VISITOR_EXPOSED_FLAG_NOT_FOUND.build(visitor.delegate) == null) + assertTrue(TroubleShooting.Factory.SDK_BUCKETING_FILE.build(visitor.delegate) == null) + assertTrue(TroubleShooting.Factory.SEND_ACTIVATE_HIT_ROUTE_ERROR.build(visitor.delegate) == null) + assertTrue(TroubleShooting.Factory.VISITOR_SEND_ACTIVATE.build(visitor.delegate) == null) + assertTrue(TroubleShooting.Factory.VISITOR_AUTHENTICATE.build(null) == null) + assertTrue(TroubleShooting.Factory.VISITOR_UNAUTHENTICATE.build(null) == null) + assertTrue(TroubleShooting.Factory.VISITOR_FETCH_CAMPAIGNS.build(null) == null) + val hit = TroubleShooting.Factory.VISITOR_FETCH_CAMPAIGNS.build(visitor.delegate) + hit!!.withVisitorIds("vid", "aid") + val json = HitCacheHelper.hitsToJSONCache(arrayListOf(hit!!)) + TroubleShooting(json[json.keys.first()]!!.getJSONObject("data")) + } + + @Test + fun test_screen_hit() { + assertTrue(Screen("ok").checkHitValidity()) + assertFalse(Screen("").checkHitValidity()) + val s = Screen("s") + s.timestamp = 1 + assertFalse(s.checkHitValidity()) + } + + @Test + fun test_segment_hit() { + assertTrue(Segment("vid", hashMapOf("un" to 1)).checkHitValidity()) + assertFalse(Segment("vid", hashMapOf()).checkHitValidity()) + val s = Segment("vid", hashMapOf("un" to 1)) + s.timestamp = 1 + assertFalse(s.checkHitValidity()) + val hit = Segment("vid", hashMapOf("un" to 1)) + hit.withVisitorIds("vid", "aid") + val json = HitCacheHelper.hitsToJSONCache(arrayListOf(hit)) + Segment(json[json.keys.first()]!!.getJSONObject("data")) + } + + @Test + fun test_event_hit() { + assertFalse(Event(Event.EventCategory.ACTION_TRACKING, "").checkHitValidity()) + val event1 = Event(Event.EventCategory.ACTION_TRACKING, "action") + event1.withEventValue(1) + event1.withEventValue(-1) + event1.withResolution(100, 100) + event1.withSessionNumber(1) + event1.withFieldAndValue(FlagshipConstants.HitKeyMap.EVENT_CATEGORY, "") + assertFalse(event1.checkHitValidity()) + assertEquals(1, event1.data.getInt(FlagshipConstants.HitKeyMap.EVENT_VALUE)) + val event2 = Event(Event.EventCategory.ACTION_TRACKING, "action") + event2.timestamp = 1 + assertFalse(event2.checkHitValidity()) + val event3 = Event(Event.EventCategory.ACTION_TRACKING, "action") + event3.withVisitorIds("vid", "aid") + val json = HitCacheHelper.hitsToJSONCache(arrayListOf(event3)) + Event(json[json.keys.first()]!!.getJSONObject("data")) + } + + @Test + fun test_hit() { + val screen = Screen("home") + screen.withId("6274870L") + screen.withFieldAndValue("a", 0) + screen.withRemovedField("a") + screen.withTimestamp(948327409L) + assertFalse(screen.data().has("a")) + assertEquals(948327409, screen.timestamp) + assertEquals("6274870L", screen.id) + screen.fromCacheJSON(JSONObject("{}")) + } + + + @Test + fun test_config_lifecycle() { + var controller: ActivityController<*>? = null + val bundle = Bundle() + bundle.putBoolean("useStop", false) + runBlocking { + CoroutineScope(Job() + Dispatchers.Main).launch { + controller = + Robolectric.buildActivity(FlagshipTestsEAI.EAIActivity::class.java).create(bundle).start() + val activity = controller?.get() as AppCompatActivity + Flagship.configManager.bindToLifeCycle(activity) + }.join() + delay(200) +// val activity = controller?.get() as AppCompatActivity +// +// CoroutineScope(Job() + Dispatchers.Main).launch { +// Flagship.configManager.bindToLifeCycle(activity) +// }.join() + delay(200) + + assertEquals(true, Flagship.configManager.trackingManager?.running!!) + CoroutineScope(Job() + Dispatchers.Main).launch { + controller?.stop() + }.join() + delay(200) + assertEquals(false, Flagship.configManager.trackingManager?.running!!) + CoroutineScope(Job() + Dispatchers.Main).launch { + controller?.destroy() + }.join() + delay(200) + assertTrue(Flagship.configManager.trackingManager == null) + } + } + + @Test + fun test_visitor_not_ready_strategy() { + val visitor = Flagship.newVisitor("vid1", true).build() + val strategy = NotReadyStrategy(visitor.delegate) + strategy.sendVisitorExposition("flag", "default", 37L) + strategy.sendHit(Screen("Home")) + strategy.sendHit( + TroubleShooting.Factory.VISITOR_EXPOSED_FLAG_NOT_FOUND.build( + visitor.delegate, + "flag", + "default" + )!! + ) + strategy.sendContextRequest() + strategy.cacheVisitor() + var eaiResult = true + runBlocking { + strategy.fetchFlags().await() + eaiResult = strategy.collectEmotionsAIEvents().await() + } + assertEquals(false, eaiResult) + } + + @Test + fun test_visitor_panic_strategy() { + val logs = arrayListOf() + val customLogManager = object : LogManager() { + override fun onLog(level: Level, tag: String, message: String) { + logs.add(message) + } + + } + runBlocking { + Flagship.start( + RuntimeEnvironment.getApplication(), _ENV_ID_, _API_KEY_, FlagshipConfig.DecisionApi() + .withLogManager(customLogManager) + ) + .await() + } + val visitor = Flagship.newVisitor("vid", true).build() + runBlocking { delay(200) } + logs.clear() + val panicStrategy = com.abtasty.flagship.visitor.PanicStrategy(visitor.delegate) + panicStrategy.updateContext(hashMapOf("a" to 0)) // 1 + panicStrategy.updateContext(FlagshipContext.APP_VERSION_CODE, 0) // 1 + panicStrategy.clearContext() // 1 + panicStrategy.sendContextRequest() // no request + panicStrategy.loadContext(hashMapOf("b" to 1)) // 0 + panicStrategy.sendConsentRequest() + panicStrategy.lookupVisitorCache() + var eaiResult = true + runBlocking { + eaiResult = panicStrategy.collectEmotionsAIEvents().await() // 1 + } + assertEquals(false, eaiResult) + assertFalse(panicStrategy.visitorDelegate.visitorContext.keys.contains("a")) + assertFalse(panicStrategy.visitorDelegate.visitorContext.keys.contains("b")) + assertEquals(4, logs.size) + } + + @Test + fun test_visitor_no_consent_strategy() { + val logs = arrayListOf() + val customLogManager = object : LogManager() { + override fun onLog(level: Level, tag: String, message: String) { + logs.add(message) + } + + } + runBlocking { + Flagship.start( + RuntimeEnvironment.getApplication(), _ENV_ID_, _API_KEY_, FlagshipConfig.DecisionApi() + .withLogManager(customLogManager) + ) + .await() + } + val visitor = Flagship.newVisitor("vid", true).build() + + val noConsentStrategy = com.abtasty.flagship.visitor.NoConsentStrategy(visitor.delegate) + noConsentStrategy.sendContextRequest() + runBlocking { delay(200) } + logs.clear() + var eaiResult = true + runBlocking { + eaiResult = noConsentStrategy.collectEmotionsAIEvents().await() // 1 + } + assertEquals(false, eaiResult) + assertEquals(1, logs.size) + } + + @Test + fun test_on_fetch_flags_status() { + FlagshipTestsHelper.interceptor() + .intercept( + CAMPAIGNS_URL.format(_ENV_ID_), + FlagshipTestsHelper.responseFromAssets(getApplication(), "api_response_1.json", 200) + ).intercept( + ARIANE_URL, + FlagshipTestsHelper.response("", 500) + ).intercept( + ACTIVATION_URL, + FlagshipTestsHelper.response("", 200) + ) + runBlocking { + Flagship.start( + RuntimeEnvironment.getApplication(), _ENV_ID_, _API_KEY_, FlagshipConfig.DecisionApi() + + ).await() + } + val statusHistory = arrayListOf() + val visitor = Flagship.newVisitor("vid", true) + .onFlagStatusChanged(onFlagStatusChanged = object : OnFlagStatusChanged { + override fun onFlagStatusChanged(newStatus: FlagStatus) { + statusHistory.add(newStatus) + super.onFlagStatusChanged(newStatus) + } + + override fun onFlagStatusFetchRequired(reason: FetchFlagsRequiredStatusReason) { + super.onFlagStatusFetchRequired(reason) + } + + override fun onFlagStatusFetched() { + super.onFlagStatusFetched() + } + }) + .build() + runBlocking { + visitor.fetchFlags().await() + } + assertEquals(FlagStatus.FETCH_REQUIRED, statusHistory[0]) + assertEquals(FlagStatus.FETCHING, statusHistory[1]) + assertEquals(FlagStatus.FETCHED, statusHistory[2]) + } + + @Test + fun test_targeting_comp() { + assertTrue(ETargetingComp.EQUALS.compareNumbers(1, 1)) + assertFalse(ETargetingComp.EQUALS.compareNumbers(0, 1)) + assertFalse(ETargetingComp.EQUALS.compareInJsonArray(2, + JSONArray("[\n" + + " \"a\",\n" + + " \"b\",\n" + + " 1\n" + + "]"))) + assertTrue(ETargetingComp.EQUALS.compareInJsonArray(1, + JSONArray("[\n" + + " \"a\",\n" + + " \"b\",\n" + + " 1\n" + + "]"))) + assertFalse(ETargetingComp.NOT_EQUALS.compareNumbers(1, 1)) + assertTrue(ETargetingComp.NOT_EQUALS.compareNumbers(0, 1)) + assertTrue(ETargetingComp.NOT_EQUALS.compareInJsonArray(2, + JSONArray("[\n" + + " \"a\",\n" + + " \"b\",\n" + + " 1\n" + + "]"))) + assertFalse(ETargetingComp.NOT_EQUALS.compareInJsonArray(1, + JSONArray("[\n" + + " \"a\",\n" + + " \"b\",\n" + + " 1\n" + + "]"))) + assertFalse(ETargetingComp.GREATER_THAN.compareNumbers(2,6)) + assertTrue(ETargetingComp.GREATER_THAN.compareNumbers(6,2)) + assertTrue(ETargetingComp.LOWER_THAN.compareNumbers(2,6)) + assertFalse(ETargetingComp.LOWER_THAN.compareNumbers(6,2)) + assertFalse(ETargetingComp.GREATER_THAN_OR_EQUALS.compareNumbers(2,6)) + assertTrue(ETargetingComp.GREATER_THAN_OR_EQUALS.compareNumbers(2,2)) + assertTrue(ETargetingComp.GREATER_THAN_OR_EQUALS.compareNumbers(6,2)) + assertFalse(ETargetingComp.GREATER_THAN_OR_EQUALS.compare("A","B")) + assertTrue(ETargetingComp.GREATER_THAN_OR_EQUALS.compare("A","A")) + assertTrue(ETargetingComp.GREATER_THAN_OR_EQUALS.compare("B","A")) + assertTrue(ETargetingComp.LOWER_THAN_OR_EQUALS.compareNumbers(2,6)) + assertTrue(ETargetingComp.LOWER_THAN_OR_EQUALS.compareNumbers(2,2)) + assertFalse(ETargetingComp.LOWER_THAN_OR_EQUALS.compareNumbers(6,2)) + assertTrue(ETargetingComp.LOWER_THAN_OR_EQUALS.compare("A","B")) + assertTrue(ETargetingComp.LOWER_THAN_OR_EQUALS.compare("A","A")) + assertFalse(ETargetingComp.LOWER_THAN_OR_EQUALS.compare("B","A")) + assertTrue(ETargetingComp.get("do_not_exists") == null) + } + + @Test + fun test_murmur() { + val result1 = getAllocationFromMurmur("variationId", "visitorId") + val result2 = getAllocationFromMurmur(null, null) + assertTrue(result1 in 0..100) + assertTrue(result2 in 0..100) + } +} \ No newline at end of file diff --git a/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsEAI.kt b/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsEAI.kt index 9a86bbd..a0006e3 100644 --- a/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsEAI.kt +++ b/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsEAI.kt @@ -20,6 +20,7 @@ import com.abtasty.flagship.AFlagshipTest.Companion.clientOverridden import com.abtasty.flagship.api.TrackingManagerConfig import com.abtasty.flagship.cache.CacheManager import com.abtasty.flagship.cache.IVisitorCacheImplementation +import com.abtasty.flagship.eai.EAIWindowCallback import com.abtasty.flagship.main.Flagship import com.abtasty.flagship.main.FlagshipConfig import com.abtasty.flagship.utils.FlagshipConstants @@ -83,12 +84,14 @@ class FlagshipTestsEAI { var visitor : Visitor? = null var logs : ArrayList? = null + var useStop = true override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main) val application = application val visitorCacheFile = savedInstanceState?.getString("visitorCache") + useStop = savedInstanceState?.getBoolean("useStop") ?: true logs = ArrayList() runBlocking { Flagship.start( @@ -96,7 +99,7 @@ class FlagshipTestsEAI { _ENV_ID_, _API_KEY_, FlagshipConfig.DecisionApi().withTrackingManagerConfig( - TrackingManagerConfig(disablePolling = true) + TrackingManagerConfig() ).withLogLevel(LogManager.Level.ALL) .withLogManager(object : LogManager() { override fun onLog(level: Level, tag: String, message: String) { @@ -135,12 +138,14 @@ class FlagshipTestsEAI { } override fun onStop() { - super.onStop() - logs?.clear() - logs = null - runBlocking { - Flagship.stop().await() + if (useStop) { + logs?.clear() + logs = null + runBlocking { + Flagship.stop().await() + } } + super.onStop() } fun eaiCollect() { @@ -383,6 +388,21 @@ class FlagshipTestsEAI { delay(300) assertTrue(controller?.get()?.logs?.contains(EAI_COLLECT_VISITOR_ALREADY_SCORED.format(controller?.get()?.visitor?.getVisitorId())) ?: false) } + + fun testWindowCallback(window: Window) { + val callback = window.callback + (callback as? EAIWindowCallback)?.let { + callback.dispatchKeyEvent(null) + callback.dispatchKeyShortcutEvent(null) + callback.dispatchTrackballEvent(null) + callback.dispatchGenericMotionEvent(null) + callback.dispatchPopulateAccessibilityEvent(null) + } ?: { + assertTrue(false) + } + } + + testWindowCallback(controller?.get()?.window!!) } @Test @@ -438,7 +458,6 @@ class FlagshipTestsEAI { FlagshipTestsHelper.interceptor().calls[EMOTION_AI_SCORING.format(_ENV_ID_, VID)]?.size ) } - } @Test @@ -750,7 +769,6 @@ class FlagshipTestsEAI { } } - fun simulateClick(x: Float, y: Float, window: Window) { // Create the initial ACTION_DOWN event val downEvent = MotionEvent.obtain( diff --git a/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsHits.kt b/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsHits.kt index d543ac9..9c4b5dc 100644 --- a/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsHits.kt +++ b/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsHits.kt @@ -764,14 +764,26 @@ class FlagshipTestsHits : AFlagshipTest() { ).await() } + runBlocking { + delay(50) + } val visitor = Flagship.newVisitor("visitor_1", true).context(hashMapOf("isVIPUser" to true)).build() + runBlocking { + delay(50) + } runBlocking { visitor.fetchFlags().await() } + runBlocking { + delay(50) + } visitor.authenticate("co_visitor_1") + runBlocking { + delay(50) + } visitor.unauthenticate() runBlocking { - delay(300) + delay(50) } // 1 Account Settings, 1 Consent, + 1 Fetch, 1 authenticate, 1 unauthenticate assertEquals(5, FlagshipTestsHelper.interceptor().calls[TROUBLESHOOTING_URL]?.size) @@ -875,12 +887,21 @@ class FlagshipTestsHits : AFlagshipTest() { runBlocking { visitor.fetchFlags().await() } + runBlocking { + delay(100) + } //// visitor.sendHit(Screen("TF")) + runBlocking { + delay(100) + } visitor.sendHit(Transaction("92749847", "checkout")) + runBlocking { + delay(100) + } visitor.getFlag("featureEnabled").value(false) runBlocking { - delay(200) + delay(100) } //// // 1 Account Settings, 1 Consent, 1 Fetch, 1 Screen, 1 Transaction, 1 Activate @@ -910,7 +931,7 @@ class FlagshipTestsHits : AFlagshipTest() { Assert.assertEquals(_ENV_ID_, cv.getString(HIT_CID)) Assert.assertEquals("SCREENVIEW", cv.getString(HIT_T)) Assert.assertEquals("null", cv.optString(HIT_CUID, "")) - Assert.assertEquals(true, cv.getString(HIT_QT).isNotBlank()) +// Assert.assertEquals(true, cv.getString(HIT_QT).isNotBlank()) Assert.assertEquals("TF", cv.getString("hit.$DOCUMENT_LOCATION")) } FlagshipTestsHelper.interceptor().calls[TROUBLESHOOTING_URL]!![4].let { @@ -1590,8 +1611,12 @@ class FlagshipTestsHits : AFlagshipTest() { } val visitor = Flagship.newVisitor("visitor_1", true).context(hashMapOf("isVIPUser" to true)).build() + runBlocking { + delay(100) + } runBlocking { visitor.fetchFlags().await() + delay(100) } try { diff --git a/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsTrackingManager.kt b/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsTrackingManager.kt index 9161dda..f3ab263 100644 --- a/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsTrackingManager.kt +++ b/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsTrackingManager.kt @@ -995,7 +995,7 @@ class FlagshipTestsTrackingManager : AFlagshipTest() { val visitorA = Flagship.newVisitor("visitor-A", true) .context(hashMapOf(Pair("testing_tracking_manager", true))) .isAuthenticated(true) - .build() + .build() //+1 Consent delay(100) @@ -1012,7 +1012,7 @@ class FlagshipTestsTrackingManager : AFlagshipTest() { assertEquals(0, cachedHits?.size) visitorA.fetchFlags().await() - val valueA = visitorA.getFlag("my_flag").value("default") + val valueA = visitorA.getFlag("my_flag").value("default") //+1 Activate delay(100) assertEquals(1, FlagshipTestsHelper.interceptor().calls[ACTIVATION_URL]?.size) diff --git a/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsVisitor.kt b/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsVisitor.kt index 7f0619d..e82d2b6 100644 --- a/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsVisitor.kt +++ b/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsVisitor.kt @@ -668,22 +668,31 @@ class FlagshipTestsVisitor : AFlagshipTest() { ).await() } //Anonymous - val visitor = Flagship.newVisitor("anonymous", true).build() + val visitor = Flagship.newVisitor("anonymous", true).build() //+1 Consent runBlocking { visitor.fetchFlags().await() } - visitor.getFlag("target").value("default") //activate + runBlocking { + delay(100) + } + - visitor.sendHit(Screen("Unit test")) + visitor.getFlag("target").value("default") //+1 activate runBlocking { - delay(200) + delay(100) + } + + visitor.sendHit(Screen("Unit test")) //+1 Screen + + runBlocking { + delay(100) } FlagshipTestsHelper.interceptor().calls(CAMPAIGNS_URL)?.let { calls -> - assert(calls.size == 1) + assertEquals(1, calls.size) calls[0].let { (request, response) -> val json = HttpCompat.requestJson(request) assertEquals(json.getString("visitorId"), "anonymous") @@ -691,7 +700,7 @@ class FlagshipTestsVisitor : AFlagshipTest() { } } FlagshipTestsHelper.interceptor().calls(ACTIVATION_URL)?.let { calls -> - assert(calls.size == 1) + assertEquals(1, calls.size) calls[0].let { (request, response) -> val json = HttpCompat.requestJson(request) val batch = json.getJSONArray("batch").getJSONObject(0) @@ -701,7 +710,7 @@ class FlagshipTestsVisitor : AFlagshipTest() { } FlagshipTestsHelper.interceptor().calls(ARIANE_URL)?.let { calls -> - assert(calls.size == 2) + assertEquals(2, calls.size) calls[1].let { (request, response) -> val json = HttpCompat.requestJson(request) Assert.assertTrue(json.getString("t") == "BATCH") @@ -716,7 +725,7 @@ class FlagshipTestsVisitor : AFlagshipTest() { } } - Thread.sleep(200) + Thread.sleep(100) //Logged FlagshipTestsHelper.interceptor().calls.clear() @@ -727,13 +736,21 @@ class FlagshipTestsVisitor : AFlagshipTest() { visitor.fetchFlags().await() } - visitor.getFlag("target").value("default") //activate + runBlocking { + delay(100) + } - visitor.sendHit(Screen("Unit test")) - Thread.sleep(200) + visitor.getFlag("target").value("default") //+1 activate + + runBlocking { + delay(100) + } + + visitor.sendHit(Screen("Unit test")) //+1 Screen + Thread.sleep(100) FlagshipTestsHelper.interceptor().calls(CAMPAIGNS_URL)?.let { calls -> - assert(calls.size == 1) + assertEquals(1, calls.size) calls[0].let { (request, response) -> val content = HttpCompat.requestJson(request) assertEquals("anonymous", content.getString("anonymousId")) @@ -741,7 +758,7 @@ class FlagshipTestsVisitor : AFlagshipTest() { } } FlagshipTestsHelper.interceptor().calls(ACTIVATION_URL)?.let { calls -> - assert(calls.size == 1) + assertEquals(1, calls.size) calls[0].let { (request, response) -> val json = HttpCompat.requestJson(request) val content = json.getJSONArray("batch").getJSONObject(0) @@ -751,7 +768,7 @@ class FlagshipTestsVisitor : AFlagshipTest() { } FlagshipTestsHelper.interceptor().calls(ARIANE_URL)?.let { calls -> - assert(calls.size == 1) + assertEquals(1, calls.size) calls[0].let { (request, response) -> val json = HttpCompat.requestJson(request) Assert.assertTrue(json.getString("t") == "BATCH") @@ -775,13 +792,18 @@ class FlagshipTestsVisitor : AFlagshipTest() { visitor.fetchFlags().await() } - visitor.getFlag("target").value("default") //activate - - visitor.sendHit(Screen("Unit test")) - Thread.sleep(200) + runBlocking { + delay(100) + } + visitor.getFlag("target").value("default") //+1 activate + runBlocking { + delay(100) + } + visitor.sendHit(Screen("Unit test")) //+1 Screen + Thread.sleep(100) FlagshipTestsHelper.interceptor().calls(CAMPAIGNS_URL)?.let { calls -> - assert(calls.size == 1) + assertEquals(1, calls.size) calls[0].let { (request, response) -> val content = HttpCompat.requestJson(request) assertEquals("null", content.optString("anonymousId")) @@ -789,7 +811,7 @@ class FlagshipTestsVisitor : AFlagshipTest() { } } FlagshipTestsHelper.interceptor().calls(ACTIVATION_URL)?.let { calls -> - assert(calls.size == 1) + assertEquals(1, calls.size) calls[0].let { (request, response) -> val json = HttpCompat.requestJson(request) val content = json.getJSONArray("batch").getJSONObject(0) @@ -799,7 +821,7 @@ class FlagshipTestsVisitor : AFlagshipTest() { } FlagshipTestsHelper.interceptor().calls(ARIANE_URL)?.let { calls -> - assert(calls.size == 1) + assertEquals(1, calls.size) calls[0].let { (request, response) -> val json = HttpCompat.requestJson(request) Assert.assertTrue(json.getString("t") == "BATCH") diff --git a/gradle.properties b/gradle.properties index d84d421..ea1a9a4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,12 +23,10 @@ android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false +mavenCentralUsername= +mavenCentralPassword= signing.keyId= signing.password= signing.secretKeyRingFile= -stagingRepositoryId= -ossrhUsername= -ossrhPassword= - -org.gradle.caching=true +org.gradle.caching=true \ No newline at end of file