From 95e9f96a1d2f79cad04c67b6e888163958140f7d Mon Sep 17 00:00:00 2001 From: raf-abtasty Date: Wed, 9 Feb 2022 10:28:45 +0100 Subject: [PATCH 01/70] fix demo app --- app/src/main/java/com/abtasty/flagshipqa/MainActivity.kt | 6 ------ .../com/abtasty/flagshipqa/ui/context/ContextViewModel.kt | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/src/main/java/com/abtasty/flagshipqa/MainActivity.kt b/app/src/main/java/com/abtasty/flagshipqa/MainActivity.kt index 78842b3..b013b0d 100644 --- a/app/src/main/java/com/abtasty/flagshipqa/MainActivity.kt +++ b/app/src/main/java/com/abtasty/flagshipqa/MainActivity.kt @@ -6,13 +6,7 @@ import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController -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.launch -import kotlinx.coroutines.newFixedThreadPoolContext -import kotlinx.coroutines.runBlocking class MainActivity : AppCompatActivity() { diff --git a/app/src/main/java/com/abtasty/flagshipqa/ui/context/ContextViewModel.kt b/app/src/main/java/com/abtasty/flagshipqa/ui/context/ContextViewModel.kt index 821ac07..f71d478 100644 --- a/app/src/main/java/com/abtasty/flagshipqa/ui/context/ContextViewModel.kt +++ b/app/src/main/java/com/abtasty/flagshipqa/ui/context/ContextViewModel.kt @@ -43,6 +43,7 @@ class ContextViewModel(val appContext: Application) : AndroidViewModel(appContex fun synchronize(success: (String) -> Unit, error: (String) -> Unit) { val context = getVisitorContext(error) + Flagship.getVisitor()?.clearContext() Flagship.getVisitor()?.updateContext(context) Flagship.getVisitor()?.synchronizeModifications()?.invokeOnCompletion { Handler(Looper.getMainLooper()).post { From c53e0b755146e3354f5d511283055be4ffa4ad15 Mon Sep 17 00:00:00 2001 From: raf-abtasty Date: Wed, 9 Feb 2022 20:03:28 +0100 Subject: [PATCH 02/70] bucketing cache fix + add cache for bucketing file --- .../com/abtasty/flagship/api/HttpManager.kt | 3 +- .../abtasty/flagship/api/TrackingManager.kt | 2 +- .../com/abtasty/flagship/cache/CacheHelper.kt | 85 ++----------- .../abtasty/flagship/decision/ApiManager.kt | 19 +-- .../flagship/decision/BucketingManager.kt | 76 +++++++++--- .../flagship/decision/DecisionManager.kt | 1 - .../flagship/decision/IDecisionManager.kt | 3 +- .../abtasty/flagship/model/VariationGroup.kt | 11 +- .../flagship/visitor/DefaultStrategy.kt | 6 +- .../abtasty/flagship/visitor/VisitorCache.kt | 112 ++++++++++++++++++ .../flagship/visitor/VisitorDelegate.kt | 3 + .../flagship/visitor/VisitorDelegateDTO.kt | 17 ++- 12 files changed, 216 insertions(+), 122 deletions(-) create mode 100644 flagship/src/main/java/com/abtasty/flagship/visitor/VisitorCache.kt 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 7063fdc..d3bee19 100644 --- a/flagship/src/main/java/com/abtasty/flagship/api/HttpManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/api/HttpManager.kt @@ -35,8 +35,7 @@ object HttpManager { fun initHttpManager() { initThreadPoolExecutor() HttpCompat.insertProviderIfNeeded() -// if (!testOn) - initHttpClient() + initHttpClient() } private fun initThreadPoolExecutor() { 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 4c0560f..82cba47 100644 --- a/flagship/src/main/java/com/abtasty/flagship/api/TrackingManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/api/TrackingManager.kt @@ -86,7 +86,7 @@ class TrackingManager { logHit(tag, response, response?.requestContent) if (response == null || response.code !in 200..204) { val json = CacheHelper.fromHit(visitorDTO, type, data, time) - visitorDTO.visitorDelegate.getStrategy().cacheHit(visitorDTO.visitorId, json) + visitorDTO.visitorStrategy?.cacheHit(visitorDTO.visitorId, json) } } } diff --git a/flagship/src/main/java/com/abtasty/flagship/cache/CacheHelper.kt b/flagship/src/main/java/com/abtasty/flagship/cache/CacheHelper.kt index 50f3ee7..fedeb39 100644 --- a/flagship/src/main/java/com/abtasty/flagship/cache/CacheHelper.kt +++ b/flagship/src/main/java/com/abtasty/flagship/cache/CacheHelper.kt @@ -14,8 +14,8 @@ import org.json.JSONObject class CacheHelper { interface CacheHitMigrationInterface { - fun applyForBatch(visitorDTO: VisitorDelegateDTO, data: JSONObject) : JSONObject? - fun applyForEvent(visitorDTO: VisitorDelegateDTO, data : JSONObject) + fun applyForBatch(visitorDelegateDTO: VisitorDelegateDTO, data: JSONObject) : JSONObject? + fun applyForEvent(visitorDelegateDTO: VisitorDelegateDTO, data : JSONObject) } interface CacheVisitorMigrationInterface { @@ -26,9 +26,9 @@ class CacheHelper { MIGRATION_1() { - override fun applyForBatch(visitorDTO: VisitorDelegateDTO, data: JSONObject) : JSONObject? { + override fun applyForBatch(visitorDelegateDTO: VisitorDelegateDTO, data: JSONObject) : JSONObject? { val dataJSON = data.getJSONObject("data") - if (dataJSON.get("visitorId") == visitorDTO.visitorId) { + if (dataJSON.get("visitorId") == visitorDelegateDTO.visitorId) { val time = dataJSON.getLong("time") if (System.currentTimeMillis() <= (time + _HIT_EXPIRATION_MS_)) { val type = dataJSON.getString("type") @@ -46,15 +46,15 @@ class CacheHelper { return null } - override fun applyForEvent(visitorDTO: VisitorDelegateDTO, data: JSONObject) { + override fun applyForEvent(visitorDelegateDTO: VisitorDelegateDTO, data: JSONObject) { val dataJSON = data.getJSONObject("data") - if (dataJSON.get("visitorId") == visitorDTO.visitorId) { // todo think anonymous + if (dataJSON.get("visitorId") == visitorDelegateDTO.visitorId) { // todo think anonymous val time = dataJSON.getLong("time") val type = dataJSON.getString("type") val content = dataJSON.getJSONObject("content") if (System.currentTimeMillis() <= (time + _HIT_EXPIRATION_MS_)) - visitorDTO.configManager.trackingManager.sendHit(visitorDTO, type, time, content) + visitorDelegateDTO.configManager.trackingManager?.sendHit(visitorDelegateDTO, type, time, content) } } }; @@ -68,35 +68,7 @@ class CacheHelper { override fun applyFromJSON(visitor: VisitorDelegate, data: JSONObject) { val dataJSON = data.getJSONObject("data") if (dataJSON.get("visitorId") == visitor.visitorId) { // todo think anonymous - dataJSON.optJSONObject("context")?.let { - for (k in it.keys()) { - visitor.getStrategy().updateContext(k, it.get(k)) - } - } - dataJSON.optJSONArray("campaigns")?.let { array -> - val iterator = array.iterator() - while (iterator.hasNext()) { - val campaignJSON = iterator.next() - if (campaignJSON.optBoolean("activated", false) && - !visitor.activatedVariations.contains(campaignJSON.getString("variationId")) - ) - visitor.activatedVariations.add(campaignJSON.getString("variationId")) - campaignJSON.optJSONObject("flags")?.let { flagJSON -> - for (k in flagJSON.keys()) { - val modification = Modification( - k, - campaignJSON.getString("campaignId"), - campaignJSON.getString("variationGroupId"), - campaignJSON.getString("variationId"), - campaignJSON.getBoolean("isReference"), - flagJSON.get(k), - campaignJSON.getString("type") - ) - visitor.modifications[k] = modification - } - } - } - } + visitor.cachedVisitor.fromCacheJSON(dataJSON) } } }; @@ -108,47 +80,6 @@ class CacheHelper { internal val _HIT_CACHE_VERSION_ = 1 internal val _HIT_EXPIRATION_MS_ = 14400000 // 4h - internal fun fromVisitor(visitorDTO: VisitorDelegateDTO): JSONObject { - val data = JSONObject() - .put("visitorId", visitorDTO.visitorId) - .put("anonymousId", visitorDTO.anonymousId) - .put("consent", visitorDTO.hasConsented) - .put("context", visitorDTO.getContextAsJson()) - .put("campaigns", modificationsToJSON(visitorDTO)) - return JSONObject() - .put("version", _VISITOR_CACHE_VERSION_) - .put("data", data) - } - - private fun modificationsToJSON(visitorDTO: VisitorDelegateDTO): JSONArray { - - val campaigns = JSONArray() - for (m in visitorDTO.modifications) { - var isCampaignSet = false - for (i in 0 until campaigns.length()) { - val campaign = campaigns.getJSONObject(i) - if (campaign.optString("campaignId") == m.value.campaignId && campaign.optString("variationGroupId") == m.value.variationGroupId && - campaign.optString("variationId") == m.value.variationId - ) { - isCampaignSet = true - campaign.optJSONObject("flags")?.put(m.value.key, m.value.value ?: JSONObject.NULL) - } - } - if (!isCampaignSet) { - campaigns.put(JSONObject() - .put("campaignId", m.value.campaignId) - .put("variationGroupId", m.value.variationGroupId) - .put("variationId", m.value.variationId) - .put("isReference", m.value.isReference) - .put("type", m.value.campaignType) - .put("activated", visitorDTO.activatedVariations.contains(m.value.variationId)) - .put("flags", JSONObject().put(m.value.key, m.value.value ?: JSONObject.NULL)) - ) - } - } - return campaigns - } - fun fromHit(visitorDelegate: VisitorDelegateDTO, type: String, hitData: JSONObject, time: Long = -1): JSONObject { return JSONObject() 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 784fb3d..78f59d0 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/ApiManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/ApiManager.kt @@ -15,6 +15,7 @@ import org.json.JSONObject import java.io.IOException import com.abtasty.flagship.main.Flagship import com.abtasty.flagship.main.Flagship.getStatus +import com.abtasty.flagship.visitor.VisitorCache import com.abtasty.flagship.visitor.VisitorDelegateDTO class ApiManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flagshipConfig) { @@ -25,27 +26,29 @@ class ApiManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flagshipCo } @Throws(IOException::class) - private fun sendCampaignRequest(visitor: VisitorDelegateDTO): ArrayList? { + private fun sendCampaignRequest(visitorDelegateDTO: VisitorDelegateDTO): ArrayList? { val json = JSONObject() val headers: HashMap = HashMap() headers["x-api-key"] = flagshipConfig.apiKey headers["x-sdk-client"] = "android" headers["x-sdk-version"] = BuildConfig.FLAGSHIP_VERSION_NAME - json.put("visitorId", visitor.visitorId) - json.put("anonymousId", visitor.anonymousId) + json.put("visitorId", visitorDelegateDTO.visitorId) + json.put("anonymousId", visitorDelegateDTO.anonymousId) json.put("trigger_hit", false) - json.put("context", visitor.getContextAsJson()) + json.put("context", visitorDelegateDTO.contextToJson()) val response: ResponseCompat = HttpManager.sendHttpRequest(HttpManager.RequestType.POST, - DECISION_API + flagshipConfig.envId + CAMPAIGNS + if (!visitor.hasConsented) CONTEXT_PARAM else "", headers, json.toString()) + DECISION_API + flagshipConfig.envId + CAMPAIGNS + if (!visitorDelegateDTO.hasConsented) CONTEXT_PARAM else "", headers, json.toString()) logResponse(response) - return if (response.code < 400) parseCampaignsResponse(response.content) else null + val results = if (response.code < 400) parseCampaignsResponse(response.content) else null + updateFlagshipStatus(if (panic) Flagship.Status.PANIC else Flagship.Status.READY) + return results } - override fun getCampaignsModifications(visitorDTO: VisitorDelegateDTO): HashMap? { + override fun getCampaignsModifications(visitorDelegateDTO : VisitorDelegateDTO): HashMap? { val campaignsModifications: HashMap = HashMap() try { - sendCampaignRequest(visitorDTO)?.let { campaigns -> + sendCampaignRequest(visitorDelegateDTO)?.let { campaigns -> for ((_, _, variationGroups) in campaigns) { for (variationGroup in variationGroups) { for (variation in variationGroup?.variations!!.values) { 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 74d307a..e2cf25d 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/BucketingManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/BucketingManager.kt @@ -1,5 +1,7 @@ package com.abtasty.flagship.decision +import android.content.Context +import android.content.SharedPreferences import com.abtasty.flagship.api.HttpManager import com.abtasty.flagship.api.IFlagshipEndpoints.Companion.BUCKETING import com.abtasty.flagship.main.Flagship @@ -10,6 +12,7 @@ import com.abtasty.flagship.model.Modification import com.abtasty.flagship.utils.FlagshipConstants import com.abtasty.flagship.utils.FlagshipLogManager import com.abtasty.flagship.utils.LogManager +import com.abtasty.flagship.visitor.VisitorCache import com.abtasty.flagship.visitor.VisitorDelegateDTO import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService @@ -18,8 +21,12 @@ import java.util.concurrent.TimeUnit class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flagshipConfig) { + private val LOCAL_DECISION_FILE = "LOCAL_DECISION_FILE" + private val LAST_MODIFIED_LOCAL_DECISION_FILE = "LOCAL_DECISION_FILE" + private var executor: ScheduledExecutorService? = null - private var last_modified: String? = null + private var lastModified: String? = null + private var localDecisionFile: String? = null private var campaigns: ArrayList = ArrayList() override fun init(listener : ((Flagship.Status) -> Unit)?) { @@ -52,20 +59,38 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag private fun updateBucketingCampaigns() { try { val headers = HashMap() - if (last_modified != null) headers["If-Modified-Since"] = last_modified!! - val response = HttpManager.sendHttpRequest(HttpManager.RequestType.GET, - String.format(BUCKETING, flagshipConfig.envId), headers, null) - logResponse(response) - if (response.code < 300) { - last_modified = response.headers?.get("Last-Modified") - parseCampaignsResponse(response.content)?.let { campaigns -> - this.campaigns = campaigns + if (lastModified == null) lastModified = loadLastModifiedLocalDecisionFile() + if (localDecisionFile == null) localDecisionFile = loadLocalDecisionFile() + if (lastModified != null) headers["If-Modified-Since"] = lastModified!! + try { + HttpManager.sendHttpRequest(HttpManager.RequestType.GET, String.format(BUCKETING, flagshipConfig.envId), headers, null) + } catch (e : Exception) { + null + } ?.let { response -> + logResponse(response) + if (response.code < 300) { + localDecisionFile = response.content + lastModified = response.headers?.get("Last-Modified") + if (lastModified != null && localDecisionFile != null) { + saveLastModifiedLocalDecisionFile(lastModified!!) + saveLocalDecisionFile(localDecisionFile!!) + } } -// updateFlagshipStatus(if (panic) Flagship.Status.PANIC else Flagship.Status.READY) } +// ?: run { +// localDecisionFile = loadLocalDecisionFile() +// } + parseLocalDecisionFile() //??? } catch (e: Exception) { FlagshipLogManager.log(FlagshipLogManager.Tag.FLAGS_FETCH, LogManager.Level.ERROR, FlagshipLogManager.exceptionToString(e) ?: "") } + updateFlagshipStatus(if (panic) Flagship.Status.PANIC else Flagship.Status.READY) + } + + private fun parseLocalDecisionFile() { + parseCampaignsResponse(localDecisionFile)?.let { campaigns -> + this.campaigns = campaigns + } } override fun stop() { @@ -76,13 +101,13 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag } } - override fun getCampaignsModifications(visitorDTO: VisitorDelegateDTO): HashMap? { + override fun getCampaignsModifications(visitorDelegateDTO: VisitorDelegateDTO): HashMap? { val campaignsModifications: HashMap = HashMap() try { for ((_, _, variationGroups) in campaigns) { for (variationGroup in variationGroups) { - if (variationGroup!!.isTargetingValid(HashMap(visitorDTO.context))) { - val variation = variationGroup.selectVariation(visitorDTO) + if (variationGroup!!.isTargetingValid(HashMap(visitorDelegateDTO.context))) { + val variation = variationGroup.selectVariation(visitorDelegateDTO) if (variation != null) { val modificationsValues = variation.getModificationsValues() if (modificationsValues != null) @@ -92,7 +117,7 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag } } } - visitorDTO.visitorDelegate.getStrategy().sendContextRequest() + visitorDelegateDTO.visitorStrategy.sendContextRequest() return campaignsModifications } catch (e: Exception) { FlagshipLogManager.log(FlagshipLogManager.Tag.FLAGS_FETCH, LogManager.Level.ERROR, FlagshipLogManager.exceptionToString(e) ?: "") @@ -100,4 +125,27 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag return null } + private fun saveLocalDecisionFile(content : String) { + val prefs = Flagship.application.getSharedPreferences(flagshipConfig.envId, Context.MODE_PRIVATE).edit() + prefs.putString(LOCAL_DECISION_FILE, content) + prefs.apply() + } + + private fun saveLastModifiedLocalDecisionFile(lastModified : String) { + val prefs = Flagship.application.getSharedPreferences(flagshipConfig.envId, Context.MODE_PRIVATE).edit() + prefs.putString(LAST_MODIFIED_LOCAL_DECISION_FILE, lastModified) + prefs.apply() + } + + private fun loadLocalDecisionFile() : String? { + val prefs = Flagship.application.getSharedPreferences(flagshipConfig.envId, Context.MODE_PRIVATE) + return prefs.getString(LOCAL_DECISION_FILE, null) + } + + private fun loadLastModifiedLocalDecisionFile() : String? { + val prefs = Flagship.application.getSharedPreferences(flagshipConfig.envId, Context.MODE_PRIVATE) + return prefs.getString(LAST_MODIFIED_LOCAL_DECISION_FILE, null) + } + + } 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 f1bce22..138aa28 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/DecisionManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/DecisionManager.kt @@ -19,7 +19,6 @@ abstract class DecisionManager(var flagshipConfig: FlagshipConfig<*>) : IDecisio try { val json = JSONObject(content) panic = json.has("panic") - updateFlagshipStatus(if (panic) Flagship.Status.PANIC else Flagship.Status.READY) if (!panic) return Campaign.parse(json.getJSONArray("campaigns")) } catch (e: Exception) { FlagshipLogManager.log( 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 710389d..e579ed3 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/IDecisionManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/IDecisionManager.kt @@ -1,8 +1,9 @@ package com.abtasty.flagship.decision import com.abtasty.flagship.model.Modification +import com.abtasty.flagship.visitor.VisitorCache import com.abtasty.flagship.visitor.VisitorDelegateDTO interface IDecisionManager { - fun getCampaignsModifications(visitorDTO: VisitorDelegateDTO) : HashMap? + fun getCampaignsModifications(visitorDelegateDTO: VisitorDelegateDTO) : HashMap? } \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt b/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt index 5d05f7b..b896168 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt @@ -4,6 +4,7 @@ import com.abtasty.flagship.utils.FlagshipConstants import com.abtasty.flagship.utils.FlagshipLogManager import com.abtasty.flagship.utils.LogManager import com.abtasty.flagship.utils.MurmurHash +import com.abtasty.flagship.visitor.VisitorCache import com.abtasty.flagship.visitor.VisitorDelegate import com.abtasty.flagship.visitor.VisitorDelegateDTO import org.json.JSONObject @@ -11,15 +12,15 @@ import org.json.JSONObject data class VariationGroup(val campaignId: String, val variationGroupId: String, val variations: LinkedHashMap?, val targetingGroups: TargetingGroups?) { - fun selectVariation(visitor: VisitorDelegateDTO): Variation? { + fun selectVariation(visitorDelegateDTO: VisitorDelegateDTO): Variation? { variations?.let { - val cachedVariation = selectVariationFromCache(visitor, variations) + val cachedVariation = selectVariationFromCache(visitorDelegateDTO, variations) if (cachedVariation != null) return cachedVariation else { var p = 0 val murmurAllocation: Int = - MurmurHash.getAllocationFromMurmur(variationGroupId, visitor.visitorId) + MurmurHash.getAllocationFromMurmur(variationGroupId, visitorDelegateDTO.visitorId) for ((variationId, variation) in variations) { if (variation.allocation > 0) { //Variation with 0% are only loaded to check if it matches one from the cache, and should be ignored otherwise. p += variation.allocation @@ -41,9 +42,9 @@ data class VariationGroup(val campaignId: String, val variationGroupId: String, return null } - fun selectVariationFromCache(visitorDTO: VisitorDelegateDTO, variations: LinkedHashMap) : Variation? { + fun selectVariationFromCache(visitorDelegateDTO: VisitorDelegateDTO, variations: LinkedHashMap) : Variation? { for ((vid, v) in variations) { - if (visitorDTO.isVariationAssigned(v.variationId)) { + if (visitorDelegateDTO.mergedCachedVisitor?.isVariationAlreadyAssigned(v.variationId) == true) { FlagshipLogManager.log(FlagshipLogManager.Tag.ALLOCATION, LogManager.Level.DEBUG, FlagshipConstants.Info.CACHED_ALLOCATION.format(v.variationId)) return v 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 449d9b9..bd3ef34 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/DefaultStrategy.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/DefaultStrategy.kt @@ -203,12 +203,12 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) override fun cacheVisitor() { - val visitorDTO = visitor.toDTO() + val visitorDelegateDTO = visitor.toDTO() Flagship.coroutineScope().launch { try { - flagshipConfig.cacheManager.visitorCacheImplementation?.cacheVisitor(visitorDTO.visitorId , CacheHelper.fromVisitor(visitorDTO)) + flagshipConfig.cacheManager.visitorCacheImplementation?.cacheVisitor(visitorDelegateDTO.visitorId , visitorDelegateDTO.mergedCachedVisitor.toCacheJSON()) } catch (e : Exception) { - logCacheException(FlagshipConstants.Errors.CACHE_IMPL_ERROR.format("cacheVisitor", visitorDTO.visitorId), e) + logCacheException(FlagshipConstants.Errors.CACHE_IMPL_ERROR.format("cacheVisitor", visitorDelegateDTO.visitorId), e) } } } diff --git a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorCache.kt b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorCache.kt new file mode 100644 index 0000000..17c76ec --- /dev/null +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorCache.kt @@ -0,0 +1,112 @@ +package com.abtasty.flagship.visitor + +import com.abtasty.flagship.cache.CacheHelper +import com.abtasty.flagship.model.Modification +import com.abtasty.flagship.model.iterator +import org.json.JSONArray +import org.json.JSONObject + +class VisitorCache(var visitorDelegate: VisitorDelegate) : VisitorDelegateDTO(visitorDelegate) { + + fun fromCacheJSON(visitorCache : JSONObject) { + this.visitorId = visitorCache.optString("visitorId") + this.anonymousId = visitorCache.optString("anonymousId") + visitorCache.optJSONObject("context")?.let { + for (k in it.keys()) { + this.context[k] = it.get(k) + } + } + this.hasConsented = visitorCache.optBoolean("consent") + visitorCache.optJSONArray("campaigns")?.let { array -> + val iterator = array.iterator() + while (iterator.hasNext()) { + val campaignJSON = iterator.next() + if (campaignJSON.optBoolean("activated", false) && + !this.activatedVariations.contains(campaignJSON.getString("variationId")) + ) + this.activatedVariations.add(campaignJSON.getString("variationId")) + campaignJSON.optJSONObject("flags")?.let { flagJSON -> + for (k in flagJSON.keys()) { + val modification = Modification( + k, + campaignJSON.getString("campaignId"), + campaignJSON.getString("variationGroupId"), + campaignJSON.getString("variationId"), + campaignJSON.getBoolean("isReference"), + flagJSON.get(k), + campaignJSON.getString("type") + ) + this.modifications[k] = modification + } + } + } + } + applyToVisitorDelegate() + } + + private fun applyToVisitorDelegate() { + visitorDelegate.getStrategy().updateContext(context) + for (e in activatedVariations) + if (!this.visitorDelegate.activatedVariations.contains(e)) + this.visitorDelegate.activatedVariations.add(e) + visitorDelegate.modifications.putAll(modifications) + } + + fun merge(visitorDelegate: VisitorDelegate) : VisitorCache { + this.visitorId = visitorDelegate.visitorId + this.anonymousId = visitorDelegate.anonymousId + this.context = HashMap(visitorDelegate.getContext()) + this.modifications.putAll(HashMap(visitorDelegate.modifications)) + for (e in visitorDelegate.activatedVariations) + if (!this.activatedVariations.contains(e)) + this.activatedVariations.add(e) + this.hasConsented = visitorDelegate.hasConsented + this.isAuthenticated = visitorDelegate.isAuthenticated + return this + } + + fun toCacheJSON() : JSONObject { + val data = JSONObject() + .put("visitorId", visitorId) + .put("anonymousId", anonymousId) + .put("consent", hasConsented) + .put("context", contextToJson()) + .put("campaigns", this.modificationsToCacheJSON()) + return JSONObject() + .put("version", CacheHelper._VISITOR_CACHE_VERSION_) + .put("data", data) + } + + private fun modificationsToCacheJSON(): JSONArray { + + val campaigns = JSONArray() + for (m in modifications) { + var isCampaignSet = false + for (i in 0 until campaigns.length()) { + val campaign = campaigns.getJSONObject(i) + if (campaign.optString("campaignId") == m.value.campaignId && campaign.optString("variationGroupId") == m.value.variationGroupId && + campaign.optString("variationId") == m.value.variationId + ) { + isCampaignSet = true + campaign.optJSONObject("flags")?.put(m.value.key, m.value.value ?: JSONObject.NULL) + } + } + if (!isCampaignSet) { + campaigns.put(JSONObject() + .put("campaignId", m.value.campaignId) + .put("variationGroupId", m.value.variationGroupId) + .put("variationId", m.value.variationId) + .put("isReference", m.value.isReference) + .put("type", m.value.campaignType) + .put("activated", activatedVariations.contains(m.value.variationId)) + .put("flags", JSONObject().put(m.value.key, m.value.value ?: JSONObject.NULL)) + ) + } + } + return campaigns + } + + internal fun isVariationAlreadyAssigned(variationId : String) : Boolean { + return modifications.any { e -> e.value.variationId == variationId } + } +} \ No newline at end of file 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 6e5e332..ba20b93 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegate.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegate.kt @@ -25,12 +25,14 @@ class VisitorDelegate(internal val configManager: ConfigManager, visitorId: Stri var activatedVariations = ConcurrentLinkedQueue() var hasConsented: Boolean var isAuthenticated: Boolean + var cachedVisitor: VisitorCache init { this.visitorId = if (visitorId == null || visitorId.isEmpty()) generateUUID() else visitorId this.isAuthenticated = isAuthenticated this.hasConsented = hasConsented anonymousId = if (isAuthenticated) generateUUID(true) else null + cachedVisitor = VisitorCache(this) getStrategy().lookupVisitorCache() getStrategy().lookupHitCache() loadContext(context) @@ -91,6 +93,7 @@ class VisitorDelegate(internal val configManager: ConfigManager, visitorId: Stri } internal fun toDTO(): VisitorDelegateDTO { + this.cachedVisitor.merge(this) return VisitorDelegateDTO(this) } } \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegateDTO.kt b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegateDTO.kt index 8b6ea94..50a970a 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegateDTO.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegateDTO.kt @@ -3,9 +3,8 @@ package com.abtasty.flagship.visitor import org.json.JSONArray import org.json.JSONObject import java.util.concurrent.ConcurrentLinkedQueue -import kotlin.collections.HashMap -class VisitorDelegateDTO(val visitorDelegate: VisitorDelegate) { +open class VisitorDelegateDTO(visitorDelegate: VisitorDelegate) { var configManager = visitorDelegate.configManager var visitorId = visitorDelegate.visitorId @@ -15,8 +14,10 @@ class VisitorDelegateDTO(val visitorDelegate: VisitorDelegate) { var activatedVariations = ConcurrentLinkedQueue(visitorDelegate.activatedVariations) var hasConsented = visitorDelegate.hasConsented var isAuthenticated = visitorDelegate.isAuthenticated + var visitorStrategy = visitorDelegate.getStrategy() + var mergedCachedVisitor = visitorDelegate.cachedVisitor - internal fun getContextAsJson(): JSONObject { + internal fun contextToJson(): JSONObject { val contextJson = JSONObject() for (e in context.entries) { contextJson.put(e.key, e.value) @@ -24,18 +25,14 @@ class VisitorDelegateDTO(val visitorDelegate: VisitorDelegate) { return contextJson } - internal fun isVariationAssigned(variationId : String) : Boolean { - return modifications.any { e -> e.value.variationId == variationId } - } - override fun toString(): String { val json = JSONObject() json.put("visitorId", visitorId) json.put("anonymousId", if (anonymousId != null) anonymousId else JSONObject.NULL) json.put("isAuthenticated", isAuthenticated) json.put("hasConsented", hasConsented) - json.put("context", getContextAsJson()) - json.put("modifications", getModificationsAsJson()) + json.put("context", contextToJson()) + json.put("modifications", modificationsToJson()) json.put("activatedVariations", activatedVariationToJsonArray(activatedVariations)) return json.toString(2) } @@ -48,7 +45,7 @@ class VisitorDelegateDTO(val visitorDelegate: VisitorDelegate) { return array } - internal fun getModificationsAsJson(): JSONObject { + internal fun modificationsToJson(): JSONObject { val modificationJson = JSONObject() for ((flag, modification) in this.modifications) { val value: Any? = modification.value From 6f3a9b7e9f4ad441c3ed5068d8d2f5e1d1ca96c0 Mon Sep 17 00:00:00 2001 From: raf-abtasty Date: Tue, 15 Feb 2022 16:53:51 +0100 Subject: [PATCH 03/70] seems ok, for bucketing cache + logs --- .../com/abtasty/flagship/api/HttpManager.kt | 10 ++++-- .../flagship/decision/BucketingManager.kt | 31 +++++++++++-------- .../flagship/utils/FlagshipConstants.kt | 4 +++ 3 files changed, 30 insertions(+), 15 deletions(-) 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 d3bee19..548d765 100644 --- a/flagship/src/main/java/com/abtasty/flagship/api/HttpManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/api/HttpManager.kt @@ -1,7 +1,9 @@ package com.abtasty.flagship.api import com.abtasty.flagship.main.Flagship +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 okhttp3.Dispatcher @@ -87,8 +89,12 @@ object HttpManager { return Flagship.coroutineScope().async { try { sendHttpRequest(type, uri, headers, content) - } catch (e: IOException) { - FlagshipLogManager.exception(e) + } catch (e: Exception) { +// FlagshipLogManager.exception(e) + FlagshipLogManager.log(FlagshipLogManager.Tag.TRACKING, LogManager.Level.ERROR, FlagshipConstants.Errors.HTTP_ERROR.format( + uri, + e.message ?: "" + )) null } } 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 e2cf25d..a7c3fb2 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/BucketingManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/BucketingManager.kt @@ -1,7 +1,6 @@ package com.abtasty.flagship.decision import android.content.Context -import android.content.SharedPreferences import com.abtasty.flagship.api.HttpManager import com.abtasty.flagship.api.IFlagshipEndpoints.Companion.BUCKETING import com.abtasty.flagship.main.Flagship @@ -10,10 +9,11 @@ import com.abtasty.flagship.main.FlagshipConfig import com.abtasty.flagship.model.Campaign import com.abtasty.flagship.model.Modification import com.abtasty.flagship.utils.FlagshipConstants +import com.abtasty.flagship.utils.FlagshipConstants.Errors.Companion.BUCKETING_POLLING_ERROR import com.abtasty.flagship.utils.FlagshipLogManager import com.abtasty.flagship.utils.LogManager -import com.abtasty.flagship.visitor.VisitorCache import com.abtasty.flagship.visitor.VisitorDelegateDTO +import org.json.JSONObject import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit @@ -22,7 +22,7 @@ import java.util.concurrent.TimeUnit class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flagshipConfig) { private val LOCAL_DECISION_FILE = "LOCAL_DECISION_FILE" - private val LAST_MODIFIED_LOCAL_DECISION_FILE = "LOCAL_DECISION_FILE" + private val LAST_MODIFIED_LOCAL_DECISION_FILE = "LAST_MODIFIED_LOCAL_DECISION_FILE" private var executor: ScheduledExecutorService? = null private var lastModified: String? = null @@ -64,9 +64,15 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag if (lastModified != null) headers["If-Modified-Since"] = lastModified!! try { HttpManager.sendHttpRequest(HttpManager.RequestType.GET, String.format(BUCKETING, flagshipConfig.envId), headers, null) - } catch (e : Exception) { + } catch (e: Exception) { + FlagshipLogManager.log(FlagshipLogManager.Tag.BUCKETING, LogManager.Level.ERROR, BUCKETING_POLLING_ERROR.format(e.message ?: "")) + localDecisionFile?.let { decisionFile -> + FlagshipLogManager.log(FlagshipLogManager.Tag.BUCKETING, LogManager.Level.INFO, FlagshipConstants.Info.BUCKETING_CACHE.format( + lastModified, + JSONObject(decisionFile).toString(4))) + } null - } ?.let { response -> + }?.let { response -> logResponse(response) if (response.code < 300) { localDecisionFile = response.content @@ -77,10 +83,7 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag } } } -// ?: run { -// localDecisionFile = loadLocalDecisionFile() -// } - parseLocalDecisionFile() //??? + parseLocalDecisionFile() } catch (e: Exception) { FlagshipLogManager.log(FlagshipLogManager.Tag.FLAGS_FETCH, LogManager.Level.ERROR, FlagshipLogManager.exceptionToString(e) ?: "") } @@ -88,8 +91,10 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag } private fun parseLocalDecisionFile() { - parseCampaignsResponse(localDecisionFile)?.let { campaigns -> - this.campaigns = campaigns + localDecisionFile?.let { content -> + parseCampaignsResponse(content)?.let { campaigns -> + this.campaigns = campaigns + } } } @@ -137,12 +142,12 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag prefs.apply() } - private fun loadLocalDecisionFile() : String? { + private fun loadLocalDecisionFile(): String? { val prefs = Flagship.application.getSharedPreferences(flagshipConfig.envId, Context.MODE_PRIVATE) return prefs.getString(LOCAL_DECISION_FILE, null) } - private fun loadLastModifiedLocalDecisionFile() : String? { + private fun loadLastModifiedLocalDecisionFile(): String? { val prefs = Flagship.application.getSharedPreferences(flagshipConfig.envId, Context.MODE_PRIVATE) return prefs.getString(LAST_MODIFIED_LOCAL_DECISION_FILE, null) } diff --git a/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipConstants.kt b/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipConstants.kt index 72bd092..5a1ac8d 100644 --- a/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipConstants.kt +++ b/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipConstants.kt @@ -36,10 +36,13 @@ class FlagshipConstants { val METHOD_DEACTIVATED_ERROR = "Method '%s' is deactivated while SDK status is: %s." val METHOD_DEACTIVATED_CONSENT_ERROR = "Method '%s' is deactivated for visitor '%s': visitor did not consent." val CONFIGURATION_POLLING_ERROR = "Setting a polling interval is only available for Bucketing configuration." + val BUCKETING_POLLING_ERROR = "An error occurred during Bucketing polling: %s." val AUTHENTICATION_BUCKETING_ERROR = "'%s' method will be ignored in Bucketing configuration." val CACHE_IMPL_ERROR = "Error: '%s' for visitor '%s' threw an exception." val CACHE_IMPL_TIMEOUT = "Error: '%s' for visitor '%s' has timed out." val CACHE_IMPL_FORMAT_ERROR = "Error: '%s' have loaded a bad format version (%d) for visitor '%s'." + + val HTTP_ERROR = "An error occured while sending request to %s: %s" } } @@ -49,6 +52,7 @@ class FlagshipConstants { //Info val READY = "Flagship SDK (version: %s) READY" val BUCKETING_INTERVAL = "Polling event." + val BUCKETING_CACHE = "Bucketing file have been loaded from cache (%s): \n%s." val NEW_ALLOCATION = "Variation %s selected with allocation %d." val CACHED_ALLOCATION = "Variation %s selected from cache." val STATUS_CHANGED = "SDK status has changed : %s." From 51e44526cfb9fa2890169ac62f720929de047038 Mon Sep 17 00:00:00 2001 From: raf-abtasty Date: Thu, 24 Feb 2022 19:11:18 +0100 Subject: [PATCH 04/70] fix bucketing realloc, need TF --- build.gradle | 2 +- .../abtasty/flagship/api/TrackingManager.kt | 5 +- .../com/abtasty/flagship/cache/CacheHelper.kt | 142 +----------------- .../abtasty/flagship/cache/HitCacheHelper.kt | 112 ++++++++++++++ .../flagship/cache/VisitorCacheHelper.kt | 138 +++++++++++++++++ .../abtasty/flagship/decision/ApiManager.kt | 8 +- .../flagship/decision/BucketingManager.kt | 3 +- .../flagship/decision/IDecisionManager.kt | 1 - .../abtasty/flagship/model/VariationGroup.kt | 56 +++---- .../flagship/utils/FlagshipConstants.kt | 2 +- .../flagship/visitor/DefaultStrategy.kt | 19 +-- .../abtasty/flagship/visitor/VisitorCache.kt | 112 -------------- .../flagship/visitor/VisitorDelegate.kt | 4 +- .../flagship/visitor/VisitorDelegateDTO.kt | 46 +++--- flagship/src/test/assets/cache_visitor.json | 4 + .../com/abtasty/flagship/FlagshipTests.kt | 6 +- 16 files changed, 331 insertions(+), 329 deletions(-) create mode 100644 flagship/src/main/java/com/abtasty/flagship/cache/HitCacheHelper.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/cache/VisitorCacheHelper.kt delete mode 100644 flagship/src/main/java/com/abtasty/flagship/visitor/VisitorCache.kt diff --git a/build.gradle b/build.gradle index 942d3ee..626b825 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ buildscript { if (flagship_version_name != null) rootProject.ext.flagship_version_name = flagship_version_name else - rootProject.ext.flagship_version_name = "3.0.0" + rootProject.ext.flagship_version_name = "3.0.1" if (flagship_version_code != null) rootProject.ext.flagship_version_code = flagship_version_code 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 82cba47..774f59e 100644 --- a/flagship/src/main/java/com/abtasty/flagship/api/TrackingManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/api/TrackingManager.kt @@ -6,6 +6,7 @@ import com.abtasty.flagship.api.IFlagshipEndpoints.Companion.ARIANE import com.abtasty.flagship.api.IFlagshipEndpoints.Companion.DECISION_API import com.abtasty.flagship.api.IFlagshipEndpoints.Companion.EVENTS import com.abtasty.flagship.cache.CacheHelper +import com.abtasty.flagship.cache.HitCacheHelper import com.abtasty.flagship.hits.Activate import com.abtasty.flagship.hits.Hit import com.abtasty.flagship.main.Flagship @@ -85,8 +86,8 @@ class TrackingManager { val tag = if (type == FlagshipLogManager.Tag.ACTIVATE.name) FlagshipLogManager.Tag.ACTIVATE else FlagshipLogManager.Tag.TRACKING logHit(tag, response, response?.requestContent) if (response == null || response.code !in 200..204) { - val json = CacheHelper.fromHit(visitorDTO, type, data, time) - visitorDTO.visitorStrategy?.cacheHit(visitorDTO.visitorId, json) + val json = HitCacheHelper.fromHit(visitorDTO, type, data, time) + visitorDTO.visitorStrategy.cacheHit(visitorDTO.visitorId, json) } } } diff --git a/flagship/src/main/java/com/abtasty/flagship/cache/CacheHelper.kt b/flagship/src/main/java/com/abtasty/flagship/cache/CacheHelper.kt index fedeb39..81835f2 100644 --- a/flagship/src/main/java/com/abtasty/flagship/cache/CacheHelper.kt +++ b/flagship/src/main/java/com/abtasty/flagship/cache/CacheHelper.kt @@ -1,148 +1,8 @@ package com.abtasty.flagship.cache -import com.abtasty.flagship.hits.Batch -import com.abtasty.flagship.model.Modification -import com.abtasty.flagship.model.iterator -import com.abtasty.flagship.utils.FlagshipConstants -import com.abtasty.flagship.utils.FlagshipLogManager -import com.abtasty.flagship.utils.LogManager -import com.abtasty.flagship.visitor.VisitorDelegate -import com.abtasty.flagship.visitor.VisitorDelegateDTO -import org.json.JSONArray -import org.json.JSONObject - -class CacheHelper { - - interface CacheHitMigrationInterface { - fun applyForBatch(visitorDelegateDTO: VisitorDelegateDTO, data: JSONObject) : JSONObject? - fun applyForEvent(visitorDelegateDTO: VisitorDelegateDTO, data : JSONObject) - } - - interface CacheVisitorMigrationInterface { - fun applyFromJSON(visitor: VisitorDelegate, data : JSONObject) - } - - enum class HitMigrations() : CacheHitMigrationInterface { - - MIGRATION_1() { - - override fun applyForBatch(visitorDelegateDTO: VisitorDelegateDTO, data: JSONObject) : JSONObject? { - val dataJSON = data.getJSONObject("data") - if (dataJSON.get("visitorId") == visitorDelegateDTO.visitorId) { - val time = dataJSON.getLong("time") - if (System.currentTimeMillis() <= (time + _HIT_EXPIRATION_MS_)) { - val type = dataJSON.getString("type") - val content = dataJSON.getJSONObject("content") - content.remove(FlagshipConstants.HitKeyMap.CLIENT_ID) - content.remove(FlagshipConstants.HitKeyMap.VISITOR_ID) - content.remove(FlagshipConstants.HitKeyMap.CUSTOM_VISITOR_ID) - content.remove(FlagshipConstants.HitKeyMap.DATA_SOURCE) - content.remove(FlagshipConstants.HitKeyMap.DEVICE_LOCALE) - content.remove(FlagshipConstants.HitKeyMap.DEVICE_RESOLUTION) - content.put(FlagshipConstants.HitKeyMap.QUEUE_TIME, System.currentTimeMillis() - time) - return content - } - } - return null - } - - override fun applyForEvent(visitorDelegateDTO: VisitorDelegateDTO, data: JSONObject) { - - val dataJSON = data.getJSONObject("data") - if (dataJSON.get("visitorId") == visitorDelegateDTO.visitorId) { // todo think anonymous - val time = dataJSON.getLong("time") - val type = dataJSON.getString("type") - val content = dataJSON.getJSONObject("content") - if (System.currentTimeMillis() <= (time + _HIT_EXPIRATION_MS_)) - visitorDelegateDTO.configManager.trackingManager?.sendHit(visitorDelegateDTO, type, time, content) - } - } - }; - } - - - enum class VisitorMigrations() : CacheVisitorMigrationInterface { - - MIGRATION_1() { - - override fun applyFromJSON(visitor: VisitorDelegate, data: JSONObject) { - val dataJSON = data.getJSONObject("data") - if (dataJSON.get("visitorId") == visitor.visitorId) { // todo think anonymous - visitor.cachedVisitor.fromCacheJSON(dataJSON) - } - } - }; - } +open class CacheHelper { companion object { - - internal val _VISITOR_CACHE_VERSION_ = 1 - internal val _HIT_CACHE_VERSION_ = 1 internal val _HIT_EXPIRATION_MS_ = 14400000 // 4h - - - fun fromHit(visitorDelegate: VisitorDelegateDTO, type: String, hitData: JSONObject, time: Long = -1): JSONObject { - return JSONObject() - .put("version", _HIT_CACHE_VERSION_) - .put("data", JSONObject() - .put("time", if (time > -1) time else System.currentTimeMillis()) - .put("visitorId", visitorDelegate.visitorId) - .put("anonymousId", visitorDelegate.anonymousId) - .put("type", type) - .put("content", hitData) - ) - } - - fun applyVisitorMigration(visitor: VisitorDelegate, data: JSONObject) { - var version = 0 - try { - if (data.keys().hasNext()) { //check if not empty - version = data.getInt("version") - VisitorMigrations.values()[version - 1].applyFromJSON(visitor, data) - } - } catch (e: Exception) { - FlagshipLogManager.log(FlagshipLogManager.Tag.CACHE, LogManager.Level.ERROR, - FlagshipConstants.Errors.CACHE_IMPL_FORMAT_ERROR.format("lookupVisitor", version, visitor.visitorId)) - } - } - - fun applyHitMigration(visitorDTO: VisitorDelegateDTO, data: JSONArray) { - - if (data.length() > 0) { //check if not empty - val batches = ArrayList() - for (e in data) { - var version = 0 - try { - version = e.getInt("version") - if (version > 0) { - val type = e.getJSONObject("data").getString("type") - when (type) { - "CONTEXT", "ACTIVATION", "BATCH" -> HitMigrations.values()[version - 1].applyForEvent(visitorDTO, e) //Send event to flagship - "SCREENVIEW", "PAGEVIEW", "EVENT", "TRANSACTION", "ITEM", "CONSENT" -> { //batch hit to ariane - HitMigrations.values()[version - 1].applyForBatch(visitorDTO, e)?.let { jsonChild -> - val batch = batches.firstOrNull { e -> e.isMaxSizeReached(jsonChild.length()) } - if (batch == null) { - val newBatch = Batch() - newBatch.addChildAsJson(jsonChild) - batches.add(newBatch) - } else { - batch.addChildAsJson(jsonChild) - } - } - } - } - } - } catch (e: Exception) { - FlagshipLogManager.log( - FlagshipLogManager.Tag.CACHE, LogManager.Level.ERROR, - FlagshipConstants.Errors.CACHE_IMPL_FORMAT_ERROR.format("lookupHits", version, visitorDTO.visitorId) - ) - } - } - for (b in batches) { - visitorDTO.configManager.trackingManager.sendHit(visitorDTO, b) - } - } - } } } \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/cache/HitCacheHelper.kt b/flagship/src/main/java/com/abtasty/flagship/cache/HitCacheHelper.kt new file mode 100644 index 0000000..5b9bd8e --- /dev/null +++ b/flagship/src/main/java/com/abtasty/flagship/cache/HitCacheHelper.kt @@ -0,0 +1,112 @@ +package com.abtasty.flagship.cache + +import com.abtasty.flagship.hits.Batch +import com.abtasty.flagship.model.iterator +import com.abtasty.flagship.utils.FlagshipConstants +import com.abtasty.flagship.utils.FlagshipLogManager +import com.abtasty.flagship.utils.LogManager +import com.abtasty.flagship.visitor.VisitorDelegateDTO +import org.json.JSONArray +import org.json.JSONObject + +class HitCacheHelper: CacheHelper() { + + interface CacheHitMigrationInterface { + fun applyForBatch(visitorDelegateDTO: VisitorDelegateDTO, data: JSONObject) : JSONObject? + fun applyForEvent(visitorDelegateDTO: VisitorDelegateDTO, data : JSONObject) + } + + enum class HitMigrations() : CacheHitMigrationInterface { + + MIGRATION_1() { + + override fun applyForBatch(visitorDelegateDTO: VisitorDelegateDTO, data: JSONObject) : JSONObject? { + val dataJSON = data.getJSONObject("data") + if (dataJSON.get("visitorId") == visitorDelegateDTO.visitorId) { + val time = dataJSON.getLong("time") + if (System.currentTimeMillis() <= (time + _HIT_EXPIRATION_MS_)) { + val type = dataJSON.getString("type") + val content = dataJSON.getJSONObject("content") + content.remove(FlagshipConstants.HitKeyMap.CLIENT_ID) + content.remove(FlagshipConstants.HitKeyMap.VISITOR_ID) + content.remove(FlagshipConstants.HitKeyMap.CUSTOM_VISITOR_ID) + content.remove(FlagshipConstants.HitKeyMap.DATA_SOURCE) + content.remove(FlagshipConstants.HitKeyMap.DEVICE_LOCALE) + content.remove(FlagshipConstants.HitKeyMap.DEVICE_RESOLUTION) + content.put(FlagshipConstants.HitKeyMap.QUEUE_TIME, System.currentTimeMillis() - time) + return content + } + } + return null + } + + override fun applyForEvent(visitorDelegateDTO: VisitorDelegateDTO, data: JSONObject) { + + val dataJSON = data.getJSONObject("data") + if (dataJSON.get("visitorId") == visitorDelegateDTO.visitorId) { // todo think anonymous + val time = dataJSON.getLong("time") + val type = dataJSON.getString("type") + val content = dataJSON.getJSONObject("content") + if (System.currentTimeMillis() <= (time + _HIT_EXPIRATION_MS_)) + visitorDelegateDTO.configManager.trackingManager?.sendHit(visitorDelegateDTO, type, time, content) + } + } + }; + } + + companion object { + + internal val _HIT_CACHE_VERSION_ = 1 + + fun fromHit(visitorDelegate: VisitorDelegateDTO, type: String, hitData: JSONObject, time: Long = -1): JSONObject { + return JSONObject() + .put("version", _HIT_CACHE_VERSION_) + .put("data", JSONObject() + .put("time", if (time > -1) time else System.currentTimeMillis()) + .put("visitorId", visitorDelegate.visitorId) + .put("anonymousId", visitorDelegate.anonymousId) + .put("type", type) + .put("content", hitData) + ) + } + + fun applyHitMigration(visitorDTO: VisitorDelegateDTO, data: JSONArray) { + + if (data.length() > 0) { //check if not empty + val batches = ArrayList() + for (e in data) { + var version = 0 + try { + version = e.getInt("version") + if (version > 0) { + val type = e.getJSONObject("data").getString("type") + when (type) { + "CONTEXT", "ACTIVATION", "BATCH" -> HitMigrations.values()[version - 1].applyForEvent(visitorDTO, e) //Send event to flagship + "SCREENVIEW", "PAGEVIEW", "EVENT", "TRANSACTION", "ITEM", "CONSENT" -> { //batch hit to ariane + HitMigrations.values()[version - 1].applyForBatch(visitorDTO, e)?.let { jsonChild -> + val batch = batches.firstOrNull { e -> e.isMaxSizeReached(jsonChild.length()) } + if (batch == null) { + val newBatch = Batch() + newBatch.addChildAsJson(jsonChild) + batches.add(newBatch) + } else { + batch.addChildAsJson(jsonChild) + } + } + } + } + } + } catch (e: Exception) { + FlagshipLogManager.log( + FlagshipLogManager.Tag.CACHE, LogManager.Level.ERROR, + FlagshipConstants.Errors.CACHE_IMPL_FORMAT_ERROR.format("lookupHits", version, visitorDTO.visitorId) + ) + } + } + for (b in batches) { + visitorDTO.configManager.trackingManager.sendHit(visitorDTO, b) + } + } + } + } +} \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/cache/VisitorCacheHelper.kt b/flagship/src/main/java/com/abtasty/flagship/cache/VisitorCacheHelper.kt new file mode 100644 index 0000000..0f8b15e --- /dev/null +++ b/flagship/src/main/java/com/abtasty/flagship/cache/VisitorCacheHelper.kt @@ -0,0 +1,138 @@ +package com.abtasty.flagship.cache + +import com.abtasty.flagship.model.Modification +import com.abtasty.flagship.model.iterator +import com.abtasty.flagship.utils.FlagshipConstants +import com.abtasty.flagship.utils.FlagshipLogManager +import com.abtasty.flagship.utils.LogManager +import com.abtasty.flagship.visitor.VisitorDelegateDTO +import org.json.JSONArray +import org.json.JSONObject + + +class VisitorCacheHelper: CacheHelper() { + + interface CacheVisitorMigrationInterface { + fun applyFromJSON(visitorDelegateDTO: VisitorDelegateDTO, data : JSONObject) + } + + enum class VisitorMigrations() : CacheVisitorMigrationInterface { + + MIGRATION_1() { + + override fun applyFromJSON(visitorDelegateDTO: VisitorDelegateDTO, data: JSONObject) { + val dataObject = data.getJSONObject("data") + val visitorDelegate = visitorDelegateDTO.visitorDelegate + if (dataObject.getString("visitorId") == visitorDelegate.visitorId) { // todo think anonymous + visitorDelegate.visitorId = dataObject.optString("visitorId") + visitorDelegate.anonymousId = dataObject.optString("anonymousId", null) + visitorDelegate.hasConsented = dataObject.optBoolean("consent", true) + dataObject.optJSONObject("context")?.let { + for (k in it.keys()) { + visitorDelegate.visitorContext[k] = it.get(k) + } + } + dataObject.optJSONArray("campaigns")?.let { array -> + val iterator = array.iterator() + while (iterator.hasNext()) { + val campaignJSON = iterator.next() + if (campaignJSON.optBoolean("activated", false) && + !visitorDelegate.activatedVariations.contains(campaignJSON.getString("variationId"))) + visitorDelegate.activatedVariations.add(campaignJSON.getString("variationId")) + campaignJSON.optJSONObject("flags")?.let { flagJSON -> + for (k in flagJSON.keys()) { + val modification = Modification( + k, + campaignJSON.getString("campaignId"), + campaignJSON.getString("variationGroupId"), + campaignJSON.getString("variationId"), + campaignJSON.getBoolean("isReference"), + flagJSON.get(k), + campaignJSON.getString("type") + ) + visitorDelegate.modifications[k] = modification + } + } + } + } + dataObject.optJSONObject("assignmentsHistory")?.let { assignmentsJson -> + for (k in assignmentsJson.keys()) + visitorDelegate.assignmentsHistory[k] = assignmentsJson.getString(k) + } + } + } + }; + } + + companion object { + + internal val _VISITOR_CACHE_VERSION_ = 1 + + fun visitorToCacheJSON(visitorDelegateDTO: VisitorDelegateDTO): JSONObject { + val data: JSONObject = JSONObject() + .put("visitorId", visitorDelegateDTO.visitorId) + .put("anonymousId", visitorDelegateDTO.anonymousId) + .put("consent", visitorDelegateDTO.hasConsented) + .put("context", visitorDelegateDTO.contextToJson()) + .put("campaigns", modificationsToCacheJSON(visitorDelegateDTO)) + .put("assignmentsHistory", assignationHistoryToCacheJSON(visitorDelegateDTO)) + return JSONObject() + .put("version", _VISITOR_CACHE_VERSION_) + .put("data", data) + } + + private fun modificationsToCacheJSON(visitorDelegateDTO: VisitorDelegateDTO): JSONArray { + + val campaigns = JSONArray() + for (m in visitorDelegateDTO.modifications) { + var isCampaignSet = false + for (i in 0 until campaigns.length()) { + val campaign = campaigns.getJSONObject(i) + if (campaign.optString("campaignId") == m.value.campaignId && campaign.optString("variationGroupId") == m.value.variationGroupId && + campaign.optString("variationId") == m.value.variationId + ) { + isCampaignSet = true + campaign.optJSONObject("flags")?.put(m.value.key, m.value.value ?: JSONObject.NULL) + } + } + if (!isCampaignSet) { + campaigns.put(JSONObject() + .put("campaignId", m.value.campaignId) + .put("variationGroupId", m.value.variationGroupId) + .put("variationId", m.value.variationId) + .put("isReference", m.value.isReference) + .put("type", m.value.campaignType) + .put("activated", visitorDelegateDTO.activatedVariations.contains(m.value.variationId)) + .put("flags", JSONObject().put(m.value.key, m.value.value ?: JSONObject.NULL)) + ) + } + } + return campaigns + } + + @Suppress("unchecked_cast") + private fun assignationHistoryToCacheJSON(visitorDelegateDTO: VisitorDelegateDTO): JSONObject { +// val assignationsJSON = JSONObject() +// for ((key, value) in visitorDelegateDTO.assignmentsHistory) { +// assignationsJSON.put(key, value) +// } +// return assignationsJSON + return JSONObject(visitorDelegateDTO.assignmentsHistory as Map) + } + + fun applyVisitorMigration(visitorDelegateDTO: VisitorDelegateDTO, data: JSONObject) { + var version = 0 + try { + if (data.keys().hasNext()) { //check if not empty + version = data.getInt("version") + VisitorMigrations.values()[version - 1].applyFromJSON(visitorDelegateDTO, data) + } + } catch (e: Exception) { + FlagshipLogManager.log( + FlagshipLogManager.Tag.CACHE, LogManager.Level.ERROR, + FlagshipConstants.Errors.CACHE_IMPL_FORMAT_ERROR.format("lookupVisitor", version, visitorDelegateDTO.visitorId)) + } + } + } + +} \ No newline at end of file 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 78f59d0..d4681d9 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/ApiManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/ApiManager.kt @@ -6,17 +6,16 @@ import com.abtasty.flagship.api.IFlagshipEndpoints.Companion.CAMPAIGNS import com.abtasty.flagship.api.IFlagshipEndpoints.Companion.CONTEXT_PARAM import com.abtasty.flagship.api.IFlagshipEndpoints.Companion.DECISION_API import com.abtasty.flagship.api.ResponseCompat +import com.abtasty.flagship.main.Flagship +import com.abtasty.flagship.main.Flagship.getStatus import com.abtasty.flagship.main.FlagshipConfig import com.abtasty.flagship.model.Campaign import com.abtasty.flagship.model.Modification import com.abtasty.flagship.utils.FlagshipLogManager import com.abtasty.flagship.utils.LogManager +import com.abtasty.flagship.visitor.VisitorDelegateDTO import org.json.JSONObject import java.io.IOException -import com.abtasty.flagship.main.Flagship -import com.abtasty.flagship.main.Flagship.getStatus -import com.abtasty.flagship.visitor.VisitorCache -import com.abtasty.flagship.visitor.VisitorDelegateDTO class ApiManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flagshipConfig) { @@ -52,6 +51,7 @@ class ApiManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flagshipCo for ((_, _, variationGroups) in campaigns) { for (variationGroup in variationGroups) { for (variation in variationGroup?.variations!!.values) { + visitorDelegateDTO.addNewAssignmentToHistory(variation.variationGroupId, variation.variationId); //save for cache variation.getModificationsValues()?.let { modificationsValues -> campaignsModifications.putAll(modificationsValues) } 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 a7c3fb2..b3b4d78 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/BucketingManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/BucketingManager.kt @@ -102,8 +102,8 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag executor?.let { executor -> if (!executor.isShutdown) executor.shutdownNow() - } + executor = null } override fun getCampaignsModifications(visitorDelegateDTO: VisitorDelegateDTO): HashMap? { @@ -114,6 +114,7 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag if (variationGroup!!.isTargetingValid(HashMap(visitorDelegateDTO.context))) { val variation = variationGroup.selectVariation(visitorDelegateDTO) if (variation != null) { + visitorDelegateDTO.addNewAssignmentToHistory(variation.variationGroupId, variation.variationId) val modificationsValues = variation.getModificationsValues() if (modificationsValues != null) campaignsModifications.putAll(modificationsValues) 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 e579ed3..661cfaf 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/IDecisionManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/IDecisionManager.kt @@ -1,7 +1,6 @@ package com.abtasty.flagship.decision import com.abtasty.flagship.model.Modification -import com.abtasty.flagship.visitor.VisitorCache import com.abtasty.flagship.visitor.VisitorDelegateDTO interface IDecisionManager { diff --git a/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt b/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt index b896168..2546a1f 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt @@ -4,8 +4,6 @@ import com.abtasty.flagship.utils.FlagshipConstants import com.abtasty.flagship.utils.FlagshipLogManager import com.abtasty.flagship.utils.LogManager import com.abtasty.flagship.utils.MurmurHash -import com.abtasty.flagship.visitor.VisitorCache -import com.abtasty.flagship.visitor.VisitorDelegate import com.abtasty.flagship.visitor.VisitorDelegateDTO import org.json.JSONObject @@ -13,27 +11,28 @@ data class VariationGroup(val campaignId: String, val variationGroupId: String, val variations: LinkedHashMap?, val targetingGroups: TargetingGroups?) { fun selectVariation(visitorDelegateDTO: VisitorDelegateDTO): Variation? { - variations?.let { - val cachedVariation = selectVariationFromCache(visitorDelegateDTO, variations) - if (cachedVariation != null) - return cachedVariation - else { - var p = 0 - val murmurAllocation: Int = - MurmurHash.getAllocationFromMurmur(variationGroupId, visitorDelegateDTO.visitorId) - for ((variationId, variation) in variations) { - if (variation.allocation > 0) { //Variation with 0% are only loaded to check if it matches one from the cache, and should be ignored otherwise. - p += variation.allocation - if (murmurAllocation < p) { - FlagshipLogManager.log( - FlagshipLogManager.Tag.ALLOCATION, - LogManager.Level.DEBUG, - FlagshipConstants.Info.NEW_ALLOCATION.format( - variation.variationId, - murmurAllocation - ) - ) - return variation + variations?.let { variations -> + val cachedVariationId = visitorDelegateDTO.getVariationGroupAssignment(variationGroupId) + val cachedVariationEntry = variations.entries.firstOrNull { e -> e.value.variationId == cachedVariationId } + when { + cachedVariationEntry != null -> { + val variation = cachedVariationEntry.value + FlagshipLogManager.log(FlagshipLogManager.Tag.ALLOCATION, LogManager.Level.DEBUG, + FlagshipConstants.Info.CACHED_ALLOCATION.format(variation.variationId)) + return variation + } + cachedVariationId != null -> return null + else -> { + var p = 0 + val murmurAllocation: Int = MurmurHash.getAllocationFromMurmur(variationGroupId, visitorDelegateDTO.visitorId) + for ((variationId, variation) in variations) { + if (variation.allocation > 0) { //Variation with 0% are only loaded to check if it matches one from the cache, and should be ignored otherwise. + p += variation.allocation + if (murmurAllocation < p) { + FlagshipLogManager.log(FlagshipLogManager.Tag.ALLOCATION, LogManager.Level.DEBUG, FlagshipConstants.Info.NEW_ALLOCATION.format( + variation.variationId, murmurAllocation)) + return variation + } } } } @@ -42,17 +41,6 @@ data class VariationGroup(val campaignId: String, val variationGroupId: String, return null } - fun selectVariationFromCache(visitorDelegateDTO: VisitorDelegateDTO, variations: LinkedHashMap) : Variation? { - for ((vid, v) in variations) { - if (visitorDelegateDTO.mergedCachedVisitor?.isVariationAlreadyAssigned(v.variationId) == true) { - FlagshipLogManager.log(FlagshipLogManager.Tag.ALLOCATION, LogManager.Level.DEBUG, - FlagshipConstants.Info.CACHED_ALLOCATION.format(v.variationId)) - return v - } - } - return null - } - fun isTargetingValid(context: HashMap): Boolean { return targetingGroups?.isTargetingValid(context) ?: true } diff --git a/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipConstants.kt b/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipConstants.kt index 5a1ac8d..355748c 100644 --- a/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipConstants.kt +++ b/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipConstants.kt @@ -42,7 +42,7 @@ class FlagshipConstants { val CACHE_IMPL_TIMEOUT = "Error: '%s' for visitor '%s' has timed out." val CACHE_IMPL_FORMAT_ERROR = "Error: '%s' have loaded a bad format version (%d) for visitor '%s'." - val HTTP_ERROR = "An error occured while sending request to %s: %s" + val HTTP_ERROR = "An error occurred while sending request to %s: %s" } } 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 bd3ef34..ad557ec 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/DefaultStrategy.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/DefaultStrategy.kt @@ -2,6 +2,8 @@ package com.abtasty.flagship.visitor import com.abtasty.flagship.api.TrackingManager import com.abtasty.flagship.cache.CacheHelper +import com.abtasty.flagship.cache.HitCacheHelper +import com.abtasty.flagship.cache.VisitorCacheHelper import com.abtasty.flagship.decision.DecisionManager import com.abtasty.flagship.hits.Activate import com.abtasty.flagship.hits.Consent @@ -206,7 +208,7 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) val visitorDelegateDTO = visitor.toDTO() Flagship.coroutineScope().launch { try { - flagshipConfig.cacheManager.visitorCacheImplementation?.cacheVisitor(visitorDelegateDTO.visitorId , visitorDelegateDTO.mergedCachedVisitor.toCacheJSON()) + flagshipConfig.cacheManager.visitorCacheImplementation?.cacheVisitor(visitorDelegateDTO.visitorId , VisitorCacheHelper.visitorToCacheJSON(visitorDelegateDTO)) } catch (e : Exception) { logCacheException(FlagshipConstants.Errors.CACHE_IMPL_ERROR.format("cacheVisitor", visitorDelegateDTO.visitorId), e) } @@ -215,15 +217,15 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) override fun lookupVisitorCache() { runBlocking { - val visitorDTO = visitor.toDTO() + val visitorDelegateDTO = visitor.toDTO() val lookupLatch = CountDownLatch(1) var result = JSONObject() val coroutine = Flagship.coroutineScope().launch { try { - result = flagshipConfig.cacheManager.visitorCacheImplementation?.lookupVisitor(visitorDTO.visitorId) ?: JSONObject() + result = flagshipConfig.cacheManager.visitorCacheImplementation?.lookupVisitor(visitorDelegateDTO.visitorId) ?: JSONObject() lookupLatch.countDown() } catch (e: Exception) { - logCacheException(FlagshipConstants.Errors.CACHE_IMPL_ERROR.format("lookupVisitor", visitorDTO.visitorId), e) + logCacheException(FlagshipConstants.Errors.CACHE_IMPL_ERROR.format("lookupVisitor", visitorDelegateDTO.visitorId), e) lookupLatch.countDown() cancel() } @@ -231,10 +233,9 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) val isSuccess = lookupLatch.await(flagshipConfig.cacheManager.visitorCacheLookupTimeout, flagshipConfig.cacheManager.timeoutUnit) if (!isSuccess) { coroutine.cancelAndJoin() - logCacheError(FlagshipConstants.Errors.CACHE_IMPL_TIMEOUT.format("lookupVisitor", visitorDTO.visitorId)) - } else { - CacheHelper.applyVisitorMigration(visitor, result) - } + logCacheError(FlagshipConstants.Errors.CACHE_IMPL_TIMEOUT.format("lookupVisitor", visitorDelegateDTO.visitorId)) + } else + VisitorCacheHelper.applyVisitorMigration(visitorDelegateDTO, result) } } @@ -269,7 +270,7 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) coroutine.cancelAndJoin() logCacheError(FlagshipConstants.Errors.CACHE_IMPL_TIMEOUT.format("lookupHits", visitorDTO.visitorId)) } else - CacheHelper.applyHitMigration(visitor.toDTO(), result) + HitCacheHelper.applyHitMigration(visitor.toDTO(), result) } } diff --git a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorCache.kt b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorCache.kt deleted file mode 100644 index 17c76ec..0000000 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorCache.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.abtasty.flagship.visitor - -import com.abtasty.flagship.cache.CacheHelper -import com.abtasty.flagship.model.Modification -import com.abtasty.flagship.model.iterator -import org.json.JSONArray -import org.json.JSONObject - -class VisitorCache(var visitorDelegate: VisitorDelegate) : VisitorDelegateDTO(visitorDelegate) { - - fun fromCacheJSON(visitorCache : JSONObject) { - this.visitorId = visitorCache.optString("visitorId") - this.anonymousId = visitorCache.optString("anonymousId") - visitorCache.optJSONObject("context")?.let { - for (k in it.keys()) { - this.context[k] = it.get(k) - } - } - this.hasConsented = visitorCache.optBoolean("consent") - visitorCache.optJSONArray("campaigns")?.let { array -> - val iterator = array.iterator() - while (iterator.hasNext()) { - val campaignJSON = iterator.next() - if (campaignJSON.optBoolean("activated", false) && - !this.activatedVariations.contains(campaignJSON.getString("variationId")) - ) - this.activatedVariations.add(campaignJSON.getString("variationId")) - campaignJSON.optJSONObject("flags")?.let { flagJSON -> - for (k in flagJSON.keys()) { - val modification = Modification( - k, - campaignJSON.getString("campaignId"), - campaignJSON.getString("variationGroupId"), - campaignJSON.getString("variationId"), - campaignJSON.getBoolean("isReference"), - flagJSON.get(k), - campaignJSON.getString("type") - ) - this.modifications[k] = modification - } - } - } - } - applyToVisitorDelegate() - } - - private fun applyToVisitorDelegate() { - visitorDelegate.getStrategy().updateContext(context) - for (e in activatedVariations) - if (!this.visitorDelegate.activatedVariations.contains(e)) - this.visitorDelegate.activatedVariations.add(e) - visitorDelegate.modifications.putAll(modifications) - } - - fun merge(visitorDelegate: VisitorDelegate) : VisitorCache { - this.visitorId = visitorDelegate.visitorId - this.anonymousId = visitorDelegate.anonymousId - this.context = HashMap(visitorDelegate.getContext()) - this.modifications.putAll(HashMap(visitorDelegate.modifications)) - for (e in visitorDelegate.activatedVariations) - if (!this.activatedVariations.contains(e)) - this.activatedVariations.add(e) - this.hasConsented = visitorDelegate.hasConsented - this.isAuthenticated = visitorDelegate.isAuthenticated - return this - } - - fun toCacheJSON() : JSONObject { - val data = JSONObject() - .put("visitorId", visitorId) - .put("anonymousId", anonymousId) - .put("consent", hasConsented) - .put("context", contextToJson()) - .put("campaigns", this.modificationsToCacheJSON()) - return JSONObject() - .put("version", CacheHelper._VISITOR_CACHE_VERSION_) - .put("data", data) - } - - private fun modificationsToCacheJSON(): JSONArray { - - val campaigns = JSONArray() - for (m in modifications) { - var isCampaignSet = false - for (i in 0 until campaigns.length()) { - val campaign = campaigns.getJSONObject(i) - if (campaign.optString("campaignId") == m.value.campaignId && campaign.optString("variationGroupId") == m.value.variationGroupId && - campaign.optString("variationId") == m.value.variationId - ) { - isCampaignSet = true - campaign.optJSONObject("flags")?.put(m.value.key, m.value.value ?: JSONObject.NULL) - } - } - if (!isCampaignSet) { - campaigns.put(JSONObject() - .put("campaignId", m.value.campaignId) - .put("variationGroupId", m.value.variationGroupId) - .put("variationId", m.value.variationId) - .put("isReference", m.value.isReference) - .put("type", m.value.campaignType) - .put("activated", activatedVariations.contains(m.value.variationId)) - .put("flags", JSONObject().put(m.value.key, m.value.value ?: JSONObject.NULL)) - ) - } - } - return campaigns - } - - internal fun isVariationAlreadyAssigned(variationId : String) : Boolean { - return modifications.any { e -> e.value.variationId == variationId } - } -} \ No newline at end of file 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 ba20b93..298c6b1 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegate.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegate.kt @@ -25,14 +25,13 @@ class VisitorDelegate(internal val configManager: ConfigManager, visitorId: Stri var activatedVariations = ConcurrentLinkedQueue() var hasConsented: Boolean var isAuthenticated: Boolean - var cachedVisitor: VisitorCache + var assignmentsHistory: ConcurrentMap = ConcurrentHashMap() init { this.visitorId = if (visitorId == null || visitorId.isEmpty()) generateUUID() else visitorId this.isAuthenticated = isAuthenticated this.hasConsented = hasConsented anonymousId = if (isAuthenticated) generateUUID(true) else null - cachedVisitor = VisitorCache(this) getStrategy().lookupVisitorCache() getStrategy().lookupHitCache() loadContext(context) @@ -93,7 +92,6 @@ class VisitorDelegate(internal val configManager: ConfigManager, visitorId: Stri } internal fun toDTO(): VisitorDelegateDTO { - this.cachedVisitor.merge(this) return VisitorDelegateDTO(this) } } \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegateDTO.kt b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegateDTO.kt index 50a970a..bfdaf5f 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegateDTO.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegateDTO.kt @@ -4,7 +4,7 @@ import org.json.JSONArray import org.json.JSONObject import java.util.concurrent.ConcurrentLinkedQueue -open class VisitorDelegateDTO(visitorDelegate: VisitorDelegate) { +open class VisitorDelegateDTO(val visitorDelegate: VisitorDelegate) { var configManager = visitorDelegate.configManager var visitorId = visitorDelegate.visitorId @@ -15,16 +15,9 @@ open class VisitorDelegateDTO(visitorDelegate: VisitorDelegate) { var hasConsented = visitorDelegate.hasConsented var isAuthenticated = visitorDelegate.isAuthenticated var visitorStrategy = visitorDelegate.getStrategy() - var mergedCachedVisitor = visitorDelegate.cachedVisitor - - internal fun contextToJson(): JSONObject { - val contextJson = JSONObject() - for (e in context.entries) { - contextJson.put(e.key, e.value) - } - return contextJson - } + var assignmentsHistory = HashMap(visitorDelegate.assignmentsHistory) + @Suppress("unchecked_cast") override fun toString(): String { val json = JSONObject() json.put("visitorId", visitorId) @@ -33,19 +26,21 @@ open class VisitorDelegateDTO(visitorDelegate: VisitorDelegate) { json.put("hasConsented", hasConsented) json.put("context", contextToJson()) json.put("modifications", modificationsToJson()) - json.put("activatedVariations", activatedVariationToJsonArray(activatedVariations)) +// json.put("activatedVariations", activatedVariationToJsonArray(activatedVariations)) + json.put("activatedVariations", JSONArray(activatedVariations)) + json.put("assignmentsHistory", JSONObject(assignmentsHistory as Map)) return json.toString(2) } - private fun activatedVariationToJsonArray(activatedVariations: ConcurrentLinkedQueue) : JSONArray { - val array = JSONArray() - for (variation in activatedVariations) { - array.put(variation) + internal fun contextToJson(): JSONObject { + val contextJson = JSONObject() + for (e in context.entries) { + contextJson.put(e.key, e.value) } - return array + return contextJson } - internal fun modificationsToJson(): JSONObject { + private fun modificationsToJson(): JSONObject { val modificationJson = JSONObject() for ((flag, modification) in this.modifications) { val value: Any? = modification.value @@ -53,4 +48,21 @@ open class VisitorDelegateDTO(visitorDelegate: VisitorDelegate) { } return modificationJson } + + fun getVariationGroupAssignment(variationGroupId: String): String? { + return assignmentsHistory[variationGroupId] + } + + fun addNewAssignmentToHistory(variationGroupId: String?, variationId: String?) { + assignmentsHistory[variationGroupId] = variationId + visitorDelegate.assignmentsHistory[variationGroupId] = variationId + } + +// private fun activatedVariationToJsonArray(activatedVariations: ConcurrentLinkedQueue) : JSONArray { +// val array = JSONArray() +// for (variation in activatedVariations) { +// array.put(variation) +// } +// return array +// } } \ No newline at end of file diff --git a/flagship/src/test/assets/cache_visitor.json b/flagship/src/test/assets/cache_visitor.json index aa3ca52..31342d4 100644 --- a/flagship/src/test/assets/cache_visitor.json +++ b/flagship/src/test/assets/cache_visitor.json @@ -18,6 +18,10 @@ "vip": true, "sdk_osName": "Android" }, + "assignmentsHistory": { + "brjjpk7734cg0sl5mmmm": "brjjpk7734cg0sl5oooo", + "bmsor064jaeg0gm4bbbb": "bmsor064jaeg0gm4dddd" + }, "campaigns": [ { "campaignId": "brjjpk7734cg0sl5llll", diff --git a/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt b/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt index 180d7ee..75ab5fe 100644 --- a/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt +++ b/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt @@ -1136,7 +1136,7 @@ class FlagshipTests { visitor.synchronizeModifications().await() Thread.sleep(100) val cachedVisitor = (visitor.configManager.flagshipConfig.cacheManager as? DefaultCacheManager)?.visitorCacheImplementation?.lookupVisitor("visitor_id") ?: JSONObject() - assertEquals(CacheHelper._VISITOR_CACHE_VERSION_, cachedVisitor.get("version")) + assertEquals(VisitorCacheHelper._VISITOR_CACHE_VERSION_, cachedVisitor.get("version")) assertEquals(true, cachedVisitor.getJSONObject("data").getBoolean("consent")) assertEquals("visitor_id", cachedVisitor.getJSONObject("data").getString("visitorId")) @@ -1296,7 +1296,7 @@ class FlagshipTests { .withVisitorCacheImplementation(object : IVisitorCacheImplementation { override fun cacheVisitor(visitorId: String, data: JSONObject) { assertEquals("visitor_id", visitorId) - assertEquals(CacheHelper._VISITOR_CACHE_VERSION_, data.get("version")) + assertEquals(VisitorCacheHelper._VISITOR_CACHE_VERSION_, data.get("version")) assertEquals(true, data.getJSONObject("data").getBoolean("consent")) assertEquals("visitor_id", data.getJSONObject("data").getString("visitorId")) assertEquals("null", data.getJSONObject("data").optString("anonymousId", "null")) @@ -1331,7 +1331,7 @@ class FlagshipTests { .withHitCacheImplementation(object: IHitCacheImplementation { override fun cacheHit(visitorId: String, data: JSONObject) { assertEquals("visitor_id", visitorId) - assertEquals(CacheHelper._HIT_CACHE_VERSION_, data.get("version")) + assertEquals(HitCacheHelper._HIT_CACHE_VERSION_, data.get("version")) val jsonData = data.getJSONObject("data") assertTrue(jsonData.getLong("time") > 0) assertEquals("visitor_id", data.getJSONObject("data").getString("visitorId")) From 8deab7eff7a5eb071bdc66fa14f99172e0ad54c7 Mon Sep 17 00:00:00 2001 From: raf-abtasty Date: Mon, 28 Feb 2022 15:40:07 +0100 Subject: [PATCH 05/70] add tu/tf for bucketing realloc --- .../com/abtasty/flagship/FlagshipTests.kt | 93 ++++++++++++++++++- .../abtasty/flagship/FlagshipTestsHelper.kt | 1 - 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt b/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt index 75ab5fe..d4fa05e 100644 --- a/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt +++ b/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt @@ -1,9 +1,9 @@ package com.abtasty.flagship import android.app.Application +import android.content.Context import android.util.Log import androidx.test.core.app.ApplicationProvider -import androidx.test.platform.app.InstrumentationRegistry import com.abtasty.flagship.api.HttpCompat import com.abtasty.flagship.api.HttpManager import com.abtasty.flagship.cache.* @@ -11,7 +11,9 @@ import com.abtasty.flagship.decision.ApiManager import com.abtasty.flagship.decision.BucketingManager import com.abtasty.flagship.hits.* import com.abtasty.flagship.main.Flagship +import com.abtasty.flagship.main.Flagship.start import com.abtasty.flagship.main.FlagshipConfig +import com.abtasty.flagship.main.FlagshipConfig.Bucketing import com.abtasty.flagship.utils.ETargetingComp import com.abtasty.flagship.utils.FlagshipContext import com.abtasty.flagship.utils.FlagshipLogManager @@ -32,6 +34,8 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.shadows.ShadowLog +import java.text.SimpleDateFormat +import java.util.* import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -1313,6 +1317,9 @@ class FlagshipTests { val jsonFlags = jsonCampaign.getJSONObject("flags") assertEquals(81111, jsonFlags.get("rank")) assertEquals(true, jsonFlags.has("rank_plus")) + val jsonHistory = data.getJSONObject("data").getJSONObject("assignmentsHistory") + assertEquals("brjjpk7734cg0sl5oooo", jsonHistory.getString("brjjpk7734cg0sl5mmmm")) + assertEquals("bmsor064jaeg0gm4dddd", jsonHistory.getString("bmsor064jaeg0gm4bbbb")) cacheVisitorLatch.countDown() } @@ -1504,4 +1511,88 @@ class FlagshipTests { // System.out.println("=> " + visitor.getFlag("rank_plus", null).value(true)) } + + @Test + public fun cache_bucketing() { + + val pref = (ApplicationProvider.getApplicationContext() as? Context)?.applicationContext?.getSharedPreferences(Flagship.getConfig().envId, Context.MODE_PRIVATE)?.edit() + pref?.clear()?.commit() + val format = SimpleDateFormat("EEE, d MMM Y hh:mm:ss", Locale.ENGLISH) + format.timeZone = TimeZone.getTimeZone("UTC") + + var timestampDate = Date(System.currentTimeMillis() - 86400000) + var date: String = format.format(timestampDate).toString() + " GMT" + + FlagshipTestsHelper.interceptor().addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(BUCKETING_URL.format(_ENV_ID_)) + .returnResponse { request, i -> + FlagshipTestsHelper.responseFromAssets(ApplicationProvider.getApplicationContext(), "bucketing_response_1.json", 200, hashMapOf( + "Last-Modified" to date + )) + } + .build()) + + val readyLatch = CountDownLatch(1) + Flagship.start(ApplicationProvider.getApplicationContext(), _ENV_ID_, _API_KEY_, Bucketing() + .withPollingIntervals(1, TimeUnit.SECONDS) + .withStatusListener { newStatus: Flagship.Status -> if (newStatus === Flagship.Status.READY) readyLatch.countDown() }) + + if (!readyLatch.await(2, TimeUnit.HOURS)) + fail() + + val prefRead = (ApplicationProvider.getApplicationContext() as? Context)?.applicationContext?.getSharedPreferences(Flagship.getConfig().envId, Context.MODE_PRIVATE); + var content = prefRead?.getString("LOCAL_DECISION_FILE", null) + var lastModified = prefRead?.getString("LAST_MODIFIED_LOCAL_DECISION_FILE", null) + + assertNotNull(content) + assertNotNull(lastModified) + + assertEquals(date, lastModified) + assertEquals(4, JSONObject(content!!).getJSONArray("campaigns").length()) + + Thread.sleep(2000) + + FlagshipTestsHelper.interceptor().clearRules() + + timestampDate = Date(System.currentTimeMillis()) + var date2 = format.format(timestampDate).toString() + " GMT" + + FlagshipTestsHelper.interceptor().addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(BUCKETING_URL.format(_ENV_ID_)) + .returnResponse { request, i -> + FlagshipTestsHelper.responseFromAssets(ApplicationProvider.getApplicationContext(), "bucketing_response_1.json", 304, hashMapOf( + "Last-Modified" to date2 + )) + } + .build()) + + + content = prefRead?.getString("LOCAL_DECISION_FILE", null) + lastModified = prefRead?.getString("LAST_MODIFIED_LOCAL_DECISION_FILE", null) + + assertNotNull(content) + assertNotNull(lastModified) + assertEquals(date, lastModified) + assertEquals(4, JSONObject(content!!).getJSONArray("campaigns").length()) + + Thread.sleep(2000) + + FlagshipTestsHelper.interceptor().clearRules() + + FlagshipTestsHelper.interceptor().addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(BUCKETING_URL.format(_ENV_ID_)) + .returnResponse { request, i -> + FlagshipTestsHelper.responseFromAssets(ApplicationProvider.getApplicationContext(), "bucketing_response_1.json", 200, hashMapOf( + "Last-Modified" to date2 + )) + } + .build()) + + Thread.sleep(2000) + + content = prefRead?.getString("LOCAL_DECISION_FILE", null) + lastModified = prefRead?.getString("LAST_MODIFIED_LOCAL_DECISION_FILE", null) + + assertNotNull(content) + assertNotNull(lastModified) + assertEquals(date2, lastModified) + assertEquals(4, JSONObject(content).getJSONArray("campaigns").length()) + } } \ No newline at end of file diff --git a/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsHelper.kt b/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsHelper.kt index 86978ea..11efeb8 100644 --- a/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsHelper.kt +++ b/flagship/src/test/java/com/abtasty/flagship/FlagshipTestsHelper.kt @@ -105,7 +105,6 @@ class FlagshipTestsHelper { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val url: String = HttpCompat.requestUrl(request) - System.out.println("#D url : " + url) return rules[url]?.proceed(request) ?: chain.proceed(request) } From ee19315bf931ffdcd962bab6753fe5e37944c075 Mon Sep 17 00:00:00 2001 From: raf-abtasty Date: Mon, 28 Feb 2022 19:02:29 +0100 Subject: [PATCH 06/70] local decision file renaming --- .../flagship/decision/BucketingManager.kt | 42 +++++++++---------- .../com/abtasty/flagship/FlagshipTests.kt | 12 +++--- 2 files changed, 27 insertions(+), 27 deletions(-) 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 b3b4d78..1d90530 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/BucketingManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/BucketingManager.kt @@ -21,12 +21,12 @@ import java.util.concurrent.TimeUnit class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flagshipConfig) { - private val LOCAL_DECISION_FILE = "LOCAL_DECISION_FILE" - private val LAST_MODIFIED_LOCAL_DECISION_FILE = "LAST_MODIFIED_LOCAL_DECISION_FILE" + private val DECISION_FILE = "DECISION_FILE" + private val LAST_MODIFIED_DECISION_FILE = "LAST_MODIFIED_DECISION_FILE" private var executor: ScheduledExecutorService? = null private var lastModified: String? = null - private var localDecisionFile: String? = null + private var decisionFile: String? = null private var campaigns: ArrayList = ArrayList() override fun init(listener : ((Flagship.Status) -> Unit)?) { @@ -59,14 +59,14 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag private fun updateBucketingCampaigns() { try { val headers = HashMap() - if (lastModified == null) lastModified = loadLastModifiedLocalDecisionFile() - if (localDecisionFile == null) localDecisionFile = loadLocalDecisionFile() + if (lastModified == null) lastModified = loadLastModifiedDecisionFile() + if (decisionFile == null) decisionFile = loadDecisionFile() if (lastModified != null) headers["If-Modified-Since"] = lastModified!! try { HttpManager.sendHttpRequest(HttpManager.RequestType.GET, String.format(BUCKETING, flagshipConfig.envId), headers, null) } catch (e: Exception) { FlagshipLogManager.log(FlagshipLogManager.Tag.BUCKETING, LogManager.Level.ERROR, BUCKETING_POLLING_ERROR.format(e.message ?: "")) - localDecisionFile?.let { decisionFile -> + decisionFile?.let { decisionFile -> FlagshipLogManager.log(FlagshipLogManager.Tag.BUCKETING, LogManager.Level.INFO, FlagshipConstants.Info.BUCKETING_CACHE.format( lastModified, JSONObject(decisionFile).toString(4))) @@ -75,23 +75,23 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag }?.let { response -> logResponse(response) if (response.code < 300) { - localDecisionFile = response.content + decisionFile = response.content lastModified = response.headers?.get("Last-Modified") - if (lastModified != null && localDecisionFile != null) { - saveLastModifiedLocalDecisionFile(lastModified!!) - saveLocalDecisionFile(localDecisionFile!!) + if (lastModified != null && decisionFile != null) { + saveLastModifiedDecisionFile(lastModified!!) + saveDecisionFile(decisionFile!!) } } } - parseLocalDecisionFile() + parseDecisionFile() } catch (e: Exception) { FlagshipLogManager.log(FlagshipLogManager.Tag.FLAGS_FETCH, LogManager.Level.ERROR, FlagshipLogManager.exceptionToString(e) ?: "") } updateFlagshipStatus(if (panic) Flagship.Status.PANIC else Flagship.Status.READY) } - private fun parseLocalDecisionFile() { - localDecisionFile?.let { content -> + private fun parseDecisionFile() { + decisionFile?.let { content -> parseCampaignsResponse(content)?.let { campaigns -> this.campaigns = campaigns } @@ -131,26 +131,26 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag return null } - private fun saveLocalDecisionFile(content : String) { + private fun saveDecisionFile(content : String) { val prefs = Flagship.application.getSharedPreferences(flagshipConfig.envId, Context.MODE_PRIVATE).edit() - prefs.putString(LOCAL_DECISION_FILE, content) + prefs.putString(DECISION_FILE, content) prefs.apply() } - private fun saveLastModifiedLocalDecisionFile(lastModified : String) { + private fun saveLastModifiedDecisionFile(lastModified : String) { val prefs = Flagship.application.getSharedPreferences(flagshipConfig.envId, Context.MODE_PRIVATE).edit() - prefs.putString(LAST_MODIFIED_LOCAL_DECISION_FILE, lastModified) + prefs.putString(LAST_MODIFIED_DECISION_FILE, lastModified) prefs.apply() } - private fun loadLocalDecisionFile(): String? { + private fun loadDecisionFile(): String? { val prefs = Flagship.application.getSharedPreferences(flagshipConfig.envId, Context.MODE_PRIVATE) - return prefs.getString(LOCAL_DECISION_FILE, null) + return prefs.getString(DECISION_FILE, null) } - private fun loadLastModifiedLocalDecisionFile(): String? { + private fun loadLastModifiedDecisionFile(): String? { val prefs = Flagship.application.getSharedPreferences(flagshipConfig.envId, Context.MODE_PRIVATE) - return prefs.getString(LAST_MODIFIED_LOCAL_DECISION_FILE, null) + return prefs.getString(LAST_MODIFIED_DECISION_FILE, null) } diff --git a/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt b/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt index d4fa05e..1f400fb 100644 --- a/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt +++ b/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt @@ -1540,8 +1540,8 @@ class FlagshipTests { fail() val prefRead = (ApplicationProvider.getApplicationContext() as? Context)?.applicationContext?.getSharedPreferences(Flagship.getConfig().envId, Context.MODE_PRIVATE); - var content = prefRead?.getString("LOCAL_DECISION_FILE", null) - var lastModified = prefRead?.getString("LAST_MODIFIED_LOCAL_DECISION_FILE", null) + var content = prefRead?.getString("DECISION_FILE", null) + var lastModified = prefRead?.getString("LAST_MODIFIED_DECISION_FILE", null) assertNotNull(content) assertNotNull(lastModified) @@ -1565,8 +1565,8 @@ class FlagshipTests { .build()) - content = prefRead?.getString("LOCAL_DECISION_FILE", null) - lastModified = prefRead?.getString("LAST_MODIFIED_LOCAL_DECISION_FILE", null) + content = prefRead?.getString("DECISION_FILE", null) + lastModified = prefRead?.getString("LAST_MODIFIED_DECISION_FILE", null) assertNotNull(content) assertNotNull(lastModified) @@ -1587,8 +1587,8 @@ class FlagshipTests { Thread.sleep(2000) - content = prefRead?.getString("LOCAL_DECISION_FILE", null) - lastModified = prefRead?.getString("LAST_MODIFIED_LOCAL_DECISION_FILE", null) + content = prefRead?.getString("DECISION_FILE", null) + lastModified = prefRead?.getString("LAST_MODIFIED_DECISION_FILE", null) assertNotNull(content) assertNotNull(lastModified) From 862f3badb90b1267513f856192e177e9515e9e43 Mon Sep 17 00:00:00 2001 From: raf-abtasty Date: Tue, 8 Mar 2022 15:17:20 +0100 Subject: [PATCH 07/70] fix hit batches length limitation --- build.gradle | 2 +- .../main/java/com/abtasty/flagship/cache/HitCacheHelper.kt | 2 +- flagship/src/main/java/com/abtasty/flagship/hits/Batch.kt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 626b825..304074b 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ buildscript { if (flagship_version_name != null) rootProject.ext.flagship_version_name = flagship_version_name else - rootProject.ext.flagship_version_name = "3.0.1" + rootProject.ext.flagship_version_name = "3.0.2" if (flagship_version_code != null) rootProject.ext.flagship_version_code = flagship_version_code diff --git a/flagship/src/main/java/com/abtasty/flagship/cache/HitCacheHelper.kt b/flagship/src/main/java/com/abtasty/flagship/cache/HitCacheHelper.kt index 5b9bd8e..4448898 100644 --- a/flagship/src/main/java/com/abtasty/flagship/cache/HitCacheHelper.kt +++ b/flagship/src/main/java/com/abtasty/flagship/cache/HitCacheHelper.kt @@ -84,7 +84,7 @@ class HitCacheHelper: CacheHelper() { "CONTEXT", "ACTIVATION", "BATCH" -> HitMigrations.values()[version - 1].applyForEvent(visitorDTO, e) //Send event to flagship "SCREENVIEW", "PAGEVIEW", "EVENT", "TRANSACTION", "ITEM", "CONSENT" -> { //batch hit to ariane HitMigrations.values()[version - 1].applyForBatch(visitorDTO, e)?.let { jsonChild -> - val batch = batches.firstOrNull { e -> e.isMaxSizeReached(jsonChild.length()) } + val batch = batches.firstOrNull { e -> e.isMaxSizeReached(jsonChild.toString().length) } if (batch == null) { val newBatch = Batch() newBatch.addChildAsJson(jsonChild) 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 a9b2e13..87e86c6 100644 --- a/flagship/src/main/java/com/abtasty/flagship/hits/Batch.kt +++ b/flagship/src/main/java/com/abtasty/flagship/hits/Batch.kt @@ -6,7 +6,7 @@ import org.json.JSONObject internal class Batch : Hit { - internal val MAX_SIZE = 2500 + internal val MAX_SIZE = 2500000 // 2,5 mb constructor() : super(Companion.Type.BATCH) { this.data.put(FlagshipConstants.HitKeyMap.HIT_BATCH, JSONArray()) @@ -18,7 +18,7 @@ internal class Batch : Hit { } fun isMaxSizeReached(lengthToAdd : Int): Boolean { - return (MAX_SIZE - this.data.length()) > lengthToAdd + return (MAX_SIZE - this.data.toString().length) > lengthToAdd } override fun checkData(): Boolean { From d6a95e8303975dc43b22a2b56f35fc0d0f85962a Mon Sep 17 00:00:00 2001 From: raf-abtasty Date: Thu, 19 May 2022 16:43:11 +0200 Subject: [PATCH 08/70] done --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 3 +-- build.gradle | 2 +- flagship/build.gradle | 15 ++++++++------- flagship/src/main/AndroidManifest.xml | 3 +-- .../abtasty/flagship/cache/VisitorCacheHelper.kt | 4 +++- .../java/com/abtasty/flagship/model/Campaign.kt | 5 +++-- .../com/abtasty/flagship/model/FlagMetadata.kt | 7 +++++-- .../com/abtasty/flagship/model/Modification.kt | 9 ++++++--- .../com/abtasty/flagship/model/Modifications.kt | 4 ++-- .../java/com/abtasty/flagship/model/Variation.kt | 4 ++-- .../com/abtasty/flagship/model/VariationGroup.kt | 6 +++--- .../src/test/assets/bucketing_response_1.json | 1 + .../java/com/abtasty/flagship/FlagshipTests.kt | 8 +++++++- gradle/wrapper/gradle-wrapper.properties | 2 +- 15 files changed, 45 insertions(+), 29 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 85efefd..1876c4d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -63,6 +63,7 @@ android { minSdkVersion 16 } } + namespace 'com.abtasty.flagshipqa' } dependencies { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fa81f94..f0ee414 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + diff --git a/build.gradle b/build.gradle index 304074b..3f2659a 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.0' + classpath 'com.android.tools.build:gradle:7.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.24.18" classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.16" diff --git a/flagship/build.gradle b/flagship/build.gradle index ab4f381..65a4a84 100644 --- a/flagship/build.gradle +++ b/flagship/build.gradle @@ -7,13 +7,13 @@ apply from: 'jacoco.gradle' android { publishNonDefault true - compileSdkVersion 31 + compileSdkVersion 32 defaultConfig { // multiDexEnabled true minSdkVersion 16 - compileSdkVersion 31 - targetSdkVersion 31 + compileSdkVersion 32 + targetSdkVersion 32 kapt { arguments { @@ -74,6 +74,7 @@ android { resValue("string", "variant", "compat") } } + namespace 'com.abtasty.flagship' } configurations { @@ -87,13 +88,13 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'androidx.test:core:1.4.0' testImplementation 'org.robolectric:robolectric:4.6' - testImplementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0' + testImplementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testCompatImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.1' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2" - implementation 'androidx.room:room-runtime:2.4.1' + implementation 'androidx.room:room-runtime:2.4.2' commonImplementation 'com.squareup.okhttp3:okhttp:4.9.3' @@ -101,8 +102,8 @@ dependencies { compatImplementation 'com.squareup.okhttp3:okhttp:3.12.12' compatImplementation 'org.conscrypt:conscrypt-android:2.5.1' - annotationProcessor("androidx.room:room-compiler:2.4.1") - kapt 'androidx.room:room-compiler:2.4.1' + annotationProcessor("androidx.room:room-compiler:2.4.2") + kapt 'androidx.room:room-compiler:2.4.2' } diff --git a/flagship/src/main/AndroidManifest.xml b/flagship/src/main/AndroidManifest.xml index 7df6467..94cbbcf 100644 --- a/flagship/src/main/AndroidManifest.xml +++ b/flagship/src/main/AndroidManifest.xml @@ -1,2 +1 @@ - + 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 0f8b15e..bdd69bd 100644 --- a/flagship/src/main/java/com/abtasty/flagship/cache/VisitorCacheHelper.kt +++ b/flagship/src/main/java/com/abtasty/flagship/cache/VisitorCacheHelper.kt @@ -48,7 +48,8 @@ class VisitorCacheHelper: CacheHelper() { campaignJSON.getString("variationId"), campaignJSON.getBoolean("isReference"), flagJSON.get(k), - campaignJSON.getString("type") + campaignJSON.getString("type"), + campaignJSON.optString("slug", "") ) visitorDelegate.modifications[k] = modification } @@ -102,6 +103,7 @@ class VisitorCacheHelper: CacheHelper() { .put("variationId", m.value.variationId) .put("isReference", m.value.isReference) .put("type", m.value.campaignType) + .put("slug", m.value.slug) .put("activated", visitorDelegateDTO.activatedVariations.contains(m.value.variationId)) .put("flags", JSONObject().put(m.value.key, m.value.value ?: JSONObject.NULL)) ) 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 ddbc2f5..198d60c 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/Campaign.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/Campaign.kt @@ -36,19 +36,20 @@ data class Campaign(val id: String, val type: String = "", val variationGroups: return try { val id = campaignObject.getString("id") val type = campaignObject.optString("type") ?: "" + val slug = if (campaignObject.isNull("slug")) "" else campaignObject.optString("slug", "") val variationGroups: LinkedList = LinkedList() val variationGroupArray = campaignObject.optJSONArray("variationGroups") variationGroupArray?.let { //bucketing for (variationGroupsObj in variationGroupArray) { val variationGroup: VariationGroup? = - VariationGroup.parse(id, type, variationGroupsObj, true) + VariationGroup.parse(id, type, slug, variationGroupsObj, true) variationGroup?.let { variationGroups.add(variationGroup) } } } ?: run { //api val variationGroup: VariationGroup? = - VariationGroup.parse(id, type, campaignObject, false) + VariationGroup.parse(id, type, slug, campaignObject, false) variationGroup?.let { variationGroups.add(variationGroup) } } Campaign(id, type, variationGroups) diff --git a/flagship/src/main/java/com/abtasty/flagship/model/FlagMetadata.kt b/flagship/src/main/java/com/abtasty/flagship/model/FlagMetadata.kt index 0e846e7..3914cd0 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/FlagMetadata.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/FlagMetadata.kt @@ -10,7 +10,8 @@ data class FlagMetadata( val variationGroupId: String = "", val variationId: String = "", val isReference: Boolean = false, - val campaignType: String = "" + val campaignType: String = "", + val slug: String = "" ) { companion object { @@ -23,7 +24,8 @@ data class FlagMetadata( modification.variationGroupId, modification.variationId, modification.isReference, - modification.campaignType + modification.campaignType, + modification.slug ) } } @@ -48,5 +50,6 @@ data class FlagMetadata( .put("variationId", variationId) .put("isReference", isReference) .put("campaignType", campaignType) + .put("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 index 9013bde..570a1ad 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/Modification.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/Modification.kt @@ -1,10 +1,13 @@ 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) { +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)" + " 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 index 924a168..63921a8 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/Modifications.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/Modifications.kt @@ -14,7 +14,7 @@ data class Modifications( companion object { - fun parse(campaignId: String, campaignType : String, variationGroupId: String, variationId: String, isReference: Boolean, modificationsObj: JSONObject): Modifications? { + 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() @@ -22,7 +22,7 @@ data class Modifications( 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) + 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) } diff --git a/flagship/src/main/java/com/abtasty/flagship/model/Variation.kt b/flagship/src/main/java/com/abtasty/flagship/model/Variation.kt index aeb7faa..33a4696 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/Variation.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/Variation.kt @@ -11,11 +11,11 @@ data class Variation(val campaignId : String, val variationGroupId : String, val companion object { - fun parse(bucketingMode : Boolean, campaignId: String, campaignType: String, variationGroupId: String, variationObj: JSONObject): Variation? { + fun parse(bucketingMode : Boolean, campaignId: String, campaignType: String, slug: String, variationGroupId: String, variationObj: JSONObject): Variation? { return try { val variationId = variationObj.getString("id") val isReference = variationObj.optBoolean("reference", false) - val modifications: Modifications? = Modifications.parse(campaignId, campaignType, variationGroupId, + val modifications: Modifications? = Modifications.parse(campaignId, campaignType, slug, variationGroupId, variationId, isReference, variationObj.getJSONObject("modifications")) val allocation = variationObj.optInt("allocation", if (bucketingMode) 0 else 100) //In Api mode always 100%, in bucketing mode the variations at 0% are loaded just to check if it matches one in cache at selection time. diff --git a/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt b/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt index 2546a1f..82bc22a 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt @@ -46,7 +46,7 @@ data class VariationGroup(val campaignId: String, val variationGroupId: String, } companion object { - fun parse(campaignId: String, campaignType: String, variationGroupsObj: JSONObject, bucketing: Boolean): VariationGroup? { + fun parse(campaignId: String, campaignType: String, slug: String, variationGroupsObj: JSONObject, bucketing: Boolean): VariationGroup? { return try { val variationGroupId = variationGroupsObj.getString(if (bucketing) "id" else "variationGroupId") @@ -55,7 +55,7 @@ data class VariationGroup(val campaignId: String, val variationGroupId: String, if (!bucketing) { // api variationGroupsObj.optJSONObject("variation")?.let { variationObj -> - Variation.parse(bucketing, campaignId, campaignType, variationGroupId, variationObj)?.let { variation -> + Variation.parse(bucketing, campaignId, campaignType, slug, variationGroupId, variationObj)?.let { variation -> variations[variation.variationId] = variation } } @@ -63,7 +63,7 @@ data class VariationGroup(val campaignId: String, val variationGroupId: String, //bucketing variationGroupsObj.optJSONArray("variations")?.let { variationArr -> for (variationObj in variationArr) { - Variation.parse(bucketing, campaignId, campaignType, variationGroupId, variationObj)?.let { variation -> + Variation.parse(bucketing, campaignId, campaignType, slug, variationGroupId, variationObj)?.let { variation -> variations[variation.variationId] = variation } } diff --git a/flagship/src/test/assets/bucketing_response_1.json b/flagship/src/test/assets/bucketing_response_1.json index 13c24e5..526d9aa 100644 --- a/flagship/src/test/assets/bucketing_response_1.json +++ b/flagship/src/test/assets/bucketing_response_1.json @@ -234,6 +234,7 @@ { "id": "brjjpk7734cg0sl5llll", "type": "ab", + "slug": "campaignSlug", "variationGroups": [ { "id": "brjjpk7734cg0sl5mmmm", diff --git a/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt b/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt index 1f400fb..32e5786 100644 --- a/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt +++ b/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt @@ -1482,7 +1482,7 @@ class FlagshipTests { assertEquals(false, rank_plus.metadata().isReference) assertEquals("ab", rank_plus.metadata().campaignType) assertEquals(true, rank_plus.metadata().exists()) - assertEquals(5, rank_plus.metadata().toJson().length()) + assertEquals(6, rank_plus.metadata().toJson().length()) val do_not_exists = visitor.getFlag("do_not_exists", "a") assertEquals("a", do_not_exists.value( false)) @@ -1508,6 +1508,12 @@ class FlagshipTests { Thread.sleep(1500) assertEquals(6, activateLatch.count) + //testing slug + + assertEquals("", visitor.getFlag("visitorIdColor", "#00000000").metadata().slug) + assertEquals("campaignSlug", visitor.getFlag("rank_plus", "#00000000").metadata().slug) + assertEquals("", visitor.getFlag("eflzjefl", "#00000000").metadata().slug) + // System.out.println("=> " + visitor.getFlag("rank_plus", null).value(true)) } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 646d339..39c17c2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip From 52424317a8e7175adceb4460600eb817a83bf5ad Mon Sep 17 00:00:00 2001 From: raf-abtasty Date: Thu, 19 May 2022 16:51:22 +0200 Subject: [PATCH 09/70] add flag java doc --- .../com/abtasty/flagship/model/FlagMetadata.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/flagship/src/main/java/com/abtasty/flagship/model/FlagMetadata.kt b/flagship/src/main/java/com/abtasty/flagship/model/FlagMetadata.kt index 3914cd0..af49bcc 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/FlagMetadata.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/FlagMetadata.kt @@ -6,11 +6,29 @@ import org.json.JSONObject * This class contains the flag campaign information. */ data class FlagMetadata( + /** + * Flag use case id. + */ val campaignId: String = "", + /** + * Flag use case variation group id. + */ val variationGroupId: String = "", + /** + * Flag use case variation id. + */ val variationId: String = "", + /** + * Is flag from the reference variation. + */ val isReference: Boolean = false, + /** + * Flag use case type + */ val campaignType: String = "", + /** + * Flag use case custom slug + */ val slug: String = "" ) { From 0b09a27b27a0fe359caf1c4d59471aa43b012bf3 Mon Sep 17 00:00:00 2001 From: raf-abtasty Date: Fri, 20 May 2022 18:43:14 +0200 Subject: [PATCH 10/70] ok for release slug --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3f2659a..31f6652 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ buildscript { if (flagship_version_name != null) rootProject.ext.flagship_version_name = flagship_version_name else - rootProject.ext.flagship_version_name = "3.0.2" + rootProject.ext.flagship_version_name = "3.0.3" if (flagship_version_code != null) rootProject.ext.flagship_version_code = flagship_version_code From ce4e301f83b873b20eb1ba05748c78cb4e646a7c Mon Sep 17 00:00:00 2001 From: = Date: Tue, 19 Jul 2022 10:31:23 +0200 Subject: [PATCH 11/70] fix Event hit value type --- build.gradle | 2 +- flagship/src/main/java/com/abtasty/flagship/hits/Event.kt | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 31f6652..233c411 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ buildscript { if (flagship_version_name != null) rootProject.ext.flagship_version_name = flagship_version_name else - rootProject.ext.flagship_version_name = "3.0.3" + rootProject.ext.flagship_version_name = "3.0.4" if (flagship_version_code != null) rootProject.ext.flagship_version_code = flagship_version_code diff --git a/flagship/src/main/java/com/abtasty/flagship/hits/Event.kt b/flagship/src/main/java/com/abtasty/flagship/hits/Event.kt index 7f068a5..1b2e664 100644 --- a/flagship/src/main/java/com/abtasty/flagship/hits/Event.kt +++ b/flagship/src/main/java/com/abtasty/flagship/hits/Event.kt @@ -31,12 +31,13 @@ open class Event(val category: EventCategory, val action : String) : Hit( } /** - * Specifies a value for this event. must be non-negative. (optional) + * Specifies a value for this event. must be non-negative Integer. (optional) * * @param value value of the event */ - fun withEventValue(value: Number): Event { - data.put(FlagshipConstants.HitKeyMap.EVENT_VALUE, value) + fun withEventValue(value: Int): Event { + if (value > 0) + data.put(FlagshipConstants.HitKeyMap.EVENT_VALUE, value) return this } From 4e228b0c17a43c1e9d1c1d5d208880b653b22f5b Mon Sep 17 00:00:00 2001 From: = Date: Tue, 19 Jul 2022 11:39:05 +0200 Subject: [PATCH 12/70] upgrade libs --- build.gradle | 2 +- flagship/build.gradle | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 233c411..bb39170 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.0' + classpath 'com.android.tools.build:gradle:7.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.24.18" classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.16" diff --git a/flagship/build.gradle b/flagship/build.gradle index 65a4a84..85f4bbb 100644 --- a/flagship/build.gradle +++ b/flagship/build.gradle @@ -7,13 +7,13 @@ apply from: 'jacoco.gradle' android { publishNonDefault true - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { // multiDexEnabled true minSdkVersion 16 - compileSdkVersion 32 - targetSdkVersion 32 + compileSdkVersion 33 + targetSdkVersion 33 kapt { arguments { @@ -88,12 +88,12 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'androidx.test:core:1.4.0' testImplementation 'org.robolectric:robolectric:4.6' - testImplementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' + testImplementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0' testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testCompatImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.1' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1" implementation 'androidx.room:room-runtime:2.4.2' commonImplementation 'com.squareup.okhttp3:okhttp:4.9.3' From 21d9de510644738eb1e3e74eb5b20bf86b4869af Mon Sep 17 00:00:00 2001 From: raf-abtasty Date: Tue, 1 Aug 2023 13:01:38 +0200 Subject: [PATCH 13/70] Test/GitHub package (#4) **Changed:** - Compilation and publishing to Maven Central --- .github/workflows/ci-unitest-build.yml | 2 +- .github/workflows/release.yml | 12 +-- .github/workflows/rollback.yml | 37 ++++--- .gitignore | 3 +- app/build.gradle | 30 +++--- .../ui/dashboard/ConfigViewModel.kt | 7 +- build.gradle | 7 +- flagship/build.gradle | 20 ++-- flagship/flagship-publishing.gradle | 99 ++++++++----------- flagship/jacoco.gradle | 6 +- .../flagship/visitor/DefaultStrategy.kt | 2 +- .../com/abtasty/flagship/FlagshipTests.kt | 69 +++++++------ gradle.properties | 11 ++- gradle/wrapper/gradle-wrapper.properties | 4 +- 14 files changed, 147 insertions(+), 162 deletions(-) diff --git a/.github/workflows/ci-unitest-build.yml b/.github/workflows/ci-unitest-build.yml index 6375a76..ed6c08f 100644 --- a/.github/workflows/ci-unitest-build.yml +++ b/.github/workflows/ci-unitest-build.yml @@ -12,7 +12,7 @@ on: jobs: test: name: Run Unit Tests - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest if: contains(github.event.head_commit.message, '#ci-auto') == false steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d153fd7..f92ba25 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ on: jobs: test: name: Run Unit Tests - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest if: contains(github.event.head_commit.message, '#ci-auto') == false steps: - uses: actions/checkout@v2 @@ -22,8 +22,8 @@ jobs: echo "FLAGSHIP_VERSION_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - name: Upload Test Report run: bash <(curl -s https://codecov.io/bash) -f "flagship/build/reports/jacoco/testCommonDebugUnitTestCoverage/testCommonDebugUnitTestCoverage.xml" - - name: Build and Publish - env: - ARTIFACTORY_USER: ${{ secrets.ARTIFACTORY_USER }} - ARTIFACTORY_KEY: ${{ secrets.ARTIFACTORY_KEY }} - run: bash ./gradlew clean :flagship:releaseAllVariantsToArtifactory \ No newline at end of file +# - name: Build and Publish +# env: +# ARTIFACTORY_USER: ${{ secrets.ARTIFACTORY_USER }} +# ARTIFACTORY_KEY: ${{ secrets.ARTIFACTORY_KEY }} +# run: bash ./gradlew clean :flagship:releaseAllVariantsToArtifactory \ No newline at end of file diff --git a/.github/workflows/rollback.yml b/.github/workflows/rollback.yml index 673ea20..77a940e 100644 --- a/.github/workflows/rollback.yml +++ b/.github/workflows/rollback.yml @@ -1,22 +1,19 @@ -# This workflow will build a Java project with Gradle -# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle - name: Artifactory rollback CI - -on: - delete: - tags: - - '*.*.*' - - '*.*' - -jobs: - proceed: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Artifactory delete version - run: | - curl --location --request DELETE 'https://abtasty.jfrog.io/artifactory/flagship-android/com/abtasty/flagship-android/${{ github.event.ref }}' --header 'Authorization: Bearer ${{ secrets.ARTIFACTORY_TOKEN }}' - curl --location --request DELETE 'https://abtasty.jfrog.io/artifactory/flagship-android-compat/com/abtasty/flagship-android-compat/${{ github.event.ref }}' --header 'Authorization: Bearer ${{ secrets.ARTIFACTORY_TOKEN }}' \ No newline at end of file +# +#on: +# delete: +# tags: +# - '*.*.*' +# - '*.*' +# +#jobs: +# proceed: +# runs-on: ubuntu-latest +# +# steps: +# - uses: actions/checkout@v2 +# - name: Artifactory delete version +# run: | +# curl --location --request DELETE 'https://abtasty.jfrog.io/artifactory/flagship-android/com/abtasty/flagship-android/${{ github.event.ref }}' --header 'Authorization: Bearer ${{ secrets.ARTIFACTORY_TOKEN }}' +# curl --location --request DELETE 'https://abtasty.jfrog.io/artifactory/flagship-android-compat/com/abtasty/flagship-android-compat/${{ github.event.ref }}' --header 'Authorization: Bearer ${{ secrets.ARTIFACTORY_TOKEN }}' \ No newline at end of file diff --git a/.gitignore b/.gitignore index c3bbc1c..ef6da67 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,6 @@ app/common/ app/keystore.properties */keystore.proprerties projectFilesBackup/* -keystore.properties -*.apk flagship/.old_git flagship/coverage-error.log +github.properties diff --git a/app/build.gradle b/app/build.gradle index 1876c4d..efa44e6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ def keystoreProperties = new Properties() keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) android { - compileSdkVersion 31 + compileSdkVersion 33 signingConfigs { release { @@ -25,8 +25,8 @@ android { defaultConfig { applicationId "com.abtasty.flagshipqa" - minSdkVersion 16 - targetSdkVersion 31 + minSdkVersion 21 + targetSdkVersion 33 versionCode 1 versionName "1.0" multiDexEnabled true @@ -45,8 +45,8 @@ android { } } compileOptions { - sourceCompatibility "1.8" - targetCompatibility "1.8" + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' @@ -70,21 +70,21 @@ dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.6.0' - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'com.google.android.material:material:1.4.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.0' - implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' - implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' + implementation 'androidx.core:core-ktx:1.10.1' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.9.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0' + implementation 'androidx.navigation:navigation-ui-ktx:2.6.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation "org.jetbrains.kotlin:kotlin-reflect:1.5.21" + implementation "org.jetbrains.kotlin:kotlin-reflect:1.8.10" implementation project(path: ':flagship') //Use local project -// implementation 'com.abtasty:flagship-android:2.0.3' //Use remote maven repository +// implementation 'com.abtasty:flagship-android:3.0.5' //Use remote maven repository testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' } \ No newline at end of file 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 d9a6569..ca22275 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 @@ -122,10 +122,9 @@ class ConfigViewModel(val appContext: Application) : AndroidViewModel(appContext fun checkParamError(): String { return when (true) { env_id.value.isNullOrEmpty() -> getStringResource(R.string.fragment_config_error_env_id) - api_key.value.isNullOrEmpty() -> - getStringResource(R.string.fragment_config_error_api_key) - (timeout.value ?: -1) <= 0 -> getStringResource(R.string.fragment_config_error_timeout) - visitorContext.value?.isNotEmpty() -> { + api_key.value.isNullOrEmpty() -> getStringResource(R.string.fragment_config_error_api_key) + ((timeout.value ?: -1) <= 0) -> getStringResource(R.string.fragment_config_error_timeout) + (visitorContext.value?.isNotEmpty()) -> { return try { JSONObject(visitorContext.value ?: "") "" diff --git a/build.gradle b/build.gradle index bb39170..cdb34f5 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { */ ext { - kotlin_version = "1.6.10" + kotlin_version = '1.8.20' artifactory_artifact_id = "flagship-android" artifactory_repo = "flagship-android" artifactory_group_id = "com.abtasty" @@ -33,7 +33,7 @@ buildscript { if (flagship_version_name != null) rootProject.ext.flagship_version_name = flagship_version_name else - rootProject.ext.flagship_version_name = "3.0.4" + rootProject.ext.flagship_version_name = "3.0.5" if (flagship_version_code != null) rootProject.ext.flagship_version_code = flagship_version_code @@ -63,7 +63,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.24.18" classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.16" @@ -74,7 +74,6 @@ allprojects { repositories { google() mavenCentral() - maven { url 'https://abtasty.jfrog.io/artifactory/flagship-android' } } } diff --git a/flagship/build.gradle b/flagship/build.gradle index 85f4bbb..a8adac8 100644 --- a/flagship/build.gradle +++ b/flagship/build.gradle @@ -10,8 +10,7 @@ android { compileSdkVersion 33 defaultConfig { -// multiDexEnabled true - minSdkVersion 16 + minSdkVersion 21 compileSdkVersion 33 targetSdkVersion 33 @@ -86,15 +85,15 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) testImplementation 'junit:junit:4.13.2' - testImplementation 'androidx.test:core:1.4.0' - testImplementation 'org.robolectric:robolectric:4.6' - testImplementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0' + testImplementation 'androidx.test:core:1.5.0' + testImplementation 'org.robolectric:robolectric:4.10' + testImplementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - testCompatImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.1' + testCompatImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.2' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1" - implementation 'androidx.room:room-runtime:2.4.2' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" + implementation 'androidx.room:room-runtime:2.5.1' commonImplementation 'com.squareup.okhttp3:okhttp:4.9.3' @@ -102,12 +101,11 @@ dependencies { compatImplementation 'com.squareup.okhttp3:okhttp:3.12.12' compatImplementation 'org.conscrypt:conscrypt-android:2.5.1' - annotationProcessor("androidx.room:room-compiler:2.4.2") - kapt 'androidx.room:room-compiler:2.4.2' + annotationProcessor("androidx.room:room-compiler:2.5.1") + kapt 'androidx.room:room-compiler:2.5.1' } - repositories { google() mavenCentral() diff --git a/flagship/flagship-publishing.gradle b/flagship/flagship-publishing.gradle index c16d7cc..398a0b2 100644 --- a/flagship/flagship-publishing.gradle +++ b/flagship/flagship-publishing.gradle @@ -1,69 +1,37 @@ +apply plugin: 'signing' apply plugin: 'maven-publish' -apply plugin: 'org.jetbrains.dokka-android' -apply plugin: "com.jfrog.artifactory" - -//./gradlew clean artifactoryPublish -artifactoryPublish.skip = true - -artifactory { - contextUrl = 'https://abtasty.jfrog.io/artifactory/' - publish { - repository { - repoKey = artifactory_artifact_id - username = artifactory_user - password = artifactory_key - maven = true - } - defaults { - publishArtifacts = true - publishPom = true - } - } -} - -task releaseAllVariantsToArtifactory { - doLast { - exec { - workingDir rootProject.projectDir - commandLine 'sh', "./gradlew", ":flagship:assembleRelease" - } - exec { - workingDir rootProject.projectDir - commandLine 'sh', "./gradlew", ":flagship:artifactoryPublish" - } - exec { - workingDir rootProject.projectDir - commandLine 'sh', "./gradlew", "flagship:artifactoryPublish", "-Dvariant=compat" - } - } -} - -task releaseAllVariantsToMavenLocal { - doLast { - exec { - workingDir rootProject.projectDir - commandLine 'sh', "./gradlew", ":flagship:assembleRelease" - } - exec { - workingDir rootProject.projectDir - commandLine 'sh', "./gradlew", ":flagship:publishToMavenLocal" - } - exec { - workingDir rootProject.projectDir - commandLine 'sh', "./gradlew", ":flagship:publishToMavenLocal", "-Dvariant=compat" - } - } -} project(':flagship') { - artifactoryPublish.dependsOn('assembleRelease') publishing { publications { - aar(MavenPublication) { + maven(MavenPublication) { groupId = artifactory_group_id artifactId = artifactory_artifact_id version = flagship_version_name artifact "$buildDir/outputs/aar/" + artifactory_artifact_id + "-" + flagship_version_name + ".aar" + pom { + name = artifactory_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 -> @@ -96,9 +64,20 @@ project(':flagship') { } } } - } - artifactoryPublish { - publications(publishing.publications.aar) + repositories { + maven { + name = "OSSRH" + url = 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/' + credentials { + username = ossrhUsername + password = ossrhPassword + } + } + } } +} + +signing { + sign publishing.publications.maven } \ No newline at end of file diff --git a/flagship/jacoco.gradle b/flagship/jacoco.gradle index 5ab0aa7..9979fa7 100644 --- a/flagship/jacoco.gradle +++ b/flagship/jacoco.gradle @@ -22,9 +22,9 @@ project.afterEvaluate { description = "Generate Jacoco coverage reports on the ${variantName.capitalize()} build." reports { - html.enabled = true - xml.enabled = true - csv.enabled = true +// html.enabled = true +// xml.enabled = true +// csv.enabled = true } def excludes = [ 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 ad557ec..6189aea 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/DefaultStrategy.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/DefaultStrategy.kt @@ -138,7 +138,7 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) override fun sendConsentRequest() { val trackingManager: TrackingManager = configManager.trackingManager - trackingManager.sendHit(visitor.toDTO(), Consent(hasConsented())) + trackingManager.sendHit(visitor.toDTO(), Consent(hasConsented())) } override fun sendHit(hit: Hit) { diff --git a/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt b/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt index 32e5786..0284449 100644 --- a/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt +++ b/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt @@ -549,15 +549,16 @@ class FlagshipTests { override fun onLog(level: Level, tag: String, message: String) { System.out.println(" ===> $tag $message") when (true) { - tag == "FLAG_USER_EXPOSED" && message.contains("deactivated") -> logLatch.countDown() - tag == "TRACKING" && message.contains("deactivated") -> logLatch.countDown() - tag == "FLAG_VALUE" && message.contains("deactivated") -> logLatch.countDown() - tag == "FLAG_METADATA" && message.contains("deactivated") -> logLatch.countDown() - tag == "UPDATE_CONTEXT" && message.contains("deactivated") -> logLatch.countDown() - tag == "AUTHENTICATE" && message.contains("deactivated") -> logLatch.countDown() - tag == "UNAUTHENTICATE" && message.contains("deactivated") -> logLatch.countDown() - tag == "CONSENT" && message.contains("deactivated") -> logLatch.countDown() - tag == "FLAGS_FETCH" && message.contains("deactivated") -> logLatch.countDown() + ((tag == "FLAG_USER_EXPOSED") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "TRACKING") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "FLAG_VALUE") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "FLAG_METADATA") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "UPDATE_CONTEXT") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "AUTHENTICATE") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "UNAUTHENTICATE") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "CONSENT") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "FLAGS_FETCH") && (message.contains("deactivated"))) -> logLatch.countDown() + else -> {} } } })) @@ -586,30 +587,33 @@ class FlagshipTests { val visitor = Flagship.newVisitor("visitor", Visitor.Instance.NEW_INSTANCE).build() assert(visitor.getModification("target", "default", true) == "default") //2 assert(visitor.getModificationInfo("target") == null) //1 - val readyLatch = CountDownLatch(1) + val bucketingLatch = CountDownLatch(1) val logLatch = CountDownLatch(9) assert(Flagship.getStatus() == Flagship.Status.NOT_INITIALIZED) - FlagshipTestsHelper.interceptor().addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(CAMPAIGNS_URL.format(_ENV_ID_)) + FlagshipTestsHelper.interceptor().addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(BUCKETING_URL.format(_ENV_ID_)) .returnResponse { request, i -> - FlagshipTestsHelper.responseFromAssets(ApplicationProvider.getApplicationContext(), "api_panic_response.json", 200) + bucketingLatch.countDown() + Thread.sleep(200) + FlagshipTestsHelper.responseFromAssets(ApplicationProvider.getApplicationContext(), "bucketing_response_1.json", 200) } .build()) Flagship.start(getApplication(),_ENV_ID_, _API_KEY_, FlagshipConfig.Bucketing().withStatusListener { status -> if (status == Flagship.Status.READY) - readyLatch.countDown() + bucketingLatch.countDown() }.withLogManager(object : LogManager() { override fun onLog(level: Level, tag: String, message: String) { System.out.println(" ===> $tag $message") when (true) { - tag == "FLAG_USER_EXPOSED" && message.contains("deactivated") -> logLatch.countDown() - tag == "TRACKING" && message.contains("deactivated") -> logLatch.countDown() - tag == "FLAG_VALUE" && message.contains("deactivated") -> logLatch.countDown() - tag == "FLAG_METADATA" && message.contains("deactivated") -> logLatch.countDown() - tag == "UPDATE_CONTEXT" && message.contains("deactivated") -> logLatch.countDown() - tag == "AUTHENTICATE" && message.contains("ignored") -> logLatch.countDown() - tag == "UNAUTHENTICATE" && message.contains("ignored") -> logLatch.countDown() - tag == "CONSENT" && message.contains("deactivated") -> logLatch.countDown() - tag == "FLAGS_FETCH" && message.contains("deactivated") -> logLatch.countDown() + ((tag == "FLAG_USER_EXPOSED") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "TRACKING") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "FLAG_VALUE") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "FLAG_METADATA") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "UPDATE_CONTEXT") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "AUTHENTICATE") && (message.contains("ignored"))) -> logLatch.countDown() + ((tag == "UNAUTHENTICATE") && (message.contains("ignored"))) -> logLatch.countDown() + ((tag == "CONSENT") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "FLAGS_FETCH") && (message.contains("deactivated"))) -> logLatch.countDown() + else -> {} } } })) @@ -625,7 +629,7 @@ class FlagshipTests { visitor.setConsent(true)//0 visitor.authenticate("logged")//1 visitor.unauthenticate() //1 - readyLatch.await(1000, TimeUnit.MILLISECONDS) + logLatch.await(1000, TimeUnit.MILLISECONDS) assertEquals(1L, logLatch.count) } @@ -682,15 +686,16 @@ class FlagshipTests { override fun onLog(level: Level, tag: String, message: String) { System.out.println(" ===> $tag $message") when (true) { - tag == "FLAG_USER_EXPOSED" && message.contains("deactivated") -> logLatch.countDown() - tag == "TRACKING" && message.contains("deactivated") -> logLatch.countDown() - tag == "FLAG_VALUE" && message.contains("deactivated") -> logLatch.countDown() - tag == "FLAG_METADATA" && message.contains("deactivated") -> logLatch.countDown() - tag == "UPDATE_CONTEXT" && message.contains("deactivated") -> logLatch.countDown() - tag == "AUTHENTICATE" && message.contains("ignored") -> logLatch.countDown() - tag == "UNAUTHENTICATE" && message.contains("ignored") -> logLatch.countDown() - tag == "CONSENT" && message.contains("deactivated") -> logLatch.countDown() - tag == "FLAGS_FETCH" && message.contains("deactivated") -> logLatch.countDown() + ((tag == "FLAG_USER_EXPOSED") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "TRACKING") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "FLAG_VALUE") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "FLAG_METADATA") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "UPDATE_CONTEXT") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "AUTHENTICATE") && (message.contains("ignored"))) -> logLatch.countDown() + ((tag == "UNAUTHENTICATE") && (message.contains("ignored"))) -> logLatch.countDown() + ((tag == "CONSENT") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "FLAGS_FETCH") && (message.contains("deactivated"))) -> logLatch.countDown() + else -> {} } } })) diff --git a/gradle.properties b/gradle.properties index 4d15d01..1c34c94 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,4 +18,13 @@ android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official \ No newline at end of file +kotlin.code.style=official +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false + +signing.keyId= +signing.password= +signing.secretKeyRingFile= +ossrhUsername= +ossrhPassword= \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 39c17c2..a2da8db 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Oct 28 12:27:56 CET 2020 +#Thu Jun 15 19:32:02 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip From dcaac656f7de02036ecc9e2c9313e5a76fa8b4f9 Mon Sep 17 00:00:00 2001 From: raf-abtasty Date: Wed, 9 Aug 2023 11:05:47 +0200 Subject: [PATCH 14/70] Disco/fs3 932 automate maven central release (#5) **Changed:** - Automate sonatype release --- .github/workflows/release.yml | 17 ++++-- build.gradle | 84 ++++++++++++----------------- flagship/build.gradle | 3 +- flagship/flagship-publishing.gradle | 16 +++--- gradle.properties | 3 +- 5 files changed, 58 insertions(+), 65 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f92ba25..9e019e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,8 +22,15 @@ jobs: echo "FLAGSHIP_VERSION_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - name: Upload Test Report run: bash <(curl -s https://codecov.io/bash) -f "flagship/build/reports/jacoco/testCommonDebugUnitTestCoverage/testCommonDebugUnitTestCoverage.xml" -# - name: Build and Publish -# env: -# ARTIFACTORY_USER: ${{ secrets.ARTIFACTORY_USER }} -# ARTIFACTORY_KEY: ${{ secrets.ARTIFACTORY_KEY }} -# run: bash ./gradlew clean :flagship:releaseAllVariantsToArtifactory \ No newline at end of file + - 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 }} + run: | + bash ./gradlew clean + bash ./gradlew flagship:assembleRelease + bash ./gradlew publishToSonatype closeSonatypeStagingRepository + bash ./gradlew publishToSonatype -Dvariant=compat closeSonatypeStagingRepository diff --git a/build.gradle b/build.gradle index cdb34f5..50f20da 100644 --- a/build.gradle +++ b/build.gradle @@ -1,72 +1,41 @@ +apply plugin: 'io.github.gradle-nexus.publish-plugin' buildscript { /* - releaseAllVariantsToBintray = - - ./gradlew assembleRelease - - ./gradlew artifactoryPublish - - ./gradlew artifactoryPublish -Dvariant=compat - - testAllVariantsWithCoverage = - ./gradlew clean testCommonDebugUnitTestCoverage - ./gradlew clean testCompatDebugUnitTestCoverage + ./gradlew publishToSonatype closeSonatypeStagingRepository + ./gradlew publishToSonatype -Dvariant=compat closeSonatypeStagingRepository */ ext { kotlin_version = '1.8.20' - artifactory_artifact_id = "flagship-android" - artifactory_repo = "flagship-android" - artifactory_group_id = "com.abtasty" - artifactory_user_org = "abtasty" - artifactory_variant = System.getProperty("variant", ""); - } - - if (!artifactory_variant.isEmpty()) { - artifactory_artifact_id = artifactory_artifact_id + '-' + artifactory_variant - } - - def flagship_version_name = System.getenv('FLAGSHIP_VERSION_NAME') - def flagship_version_code = System.getenv('FLAGSHIP_VERSION_CODE') - - def artifactory_user = System.getenv('ARTIFACTORY_USER') - def artifactory_key = System.getenv('ARTIFACTORY_KEY') - - if (flagship_version_name != null) - rootProject.ext.flagship_version_name = flagship_version_name - else - rootProject.ext.flagship_version_name = "3.0.5" - - if (flagship_version_code != null) - rootProject.ext.flagship_version_code = flagship_version_code - else - rootProject.ext.flagship_version_code = 14 - - if (artifactory_user != null && artifactory_key != null) { - rootProject.ext.artifactory_user = artifactory_user - rootProject.ext.artifactory_key = artifactory_key - } else { - try { - def keystorePropertiesFile = file(getProjectDir().absolutePath+ "/keystore.properties") - def keystoreProperties = new Properties() - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) - - rootProject.ext.artifactory_user = keystoreProperties.getProperty("artifactory_user") - rootProject.ext.artifactory_key = keystoreProperties.getProperty("artifactory_key") - - } catch (Exception e) { - rootProject.ext.artifactory_user = '' - rootProject.ext.artifactory_key = '' + maven_artifact_id = "flagship-android" + maven_repo = "flagship-android" + maven_group_id = "com.abtasty" + maven_user_org = "abtasty" + maven_variant = System.getProperty("variant", "") + if (!maven_variant.isEmpty()) { + maven_artifact_id = maven_artifact_id + '-' + maven_variant } + flagship_version_name = System.getenv('FLAGSHIP_VERSION_NAME') ?: "3.0.6" + flagship_version_code = System.getenv('FLAGSHIP_VERSION_CODE') ?: 15 + 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 } repositories { google() mavenCentral() + maven { url 'https://jitpack.io' } } dependencies { classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.24.18" classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.16" + classpath "io.github.gradle-nexus:publish-plugin:1.3.0" } } @@ -77,6 +46,19 @@ 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 -} \ No newline at end of file +} + diff --git a/flagship/build.gradle b/flagship/build.gradle index a8adac8..b0fade0 100644 --- a/flagship/build.gradle +++ b/flagship/build.gradle @@ -2,9 +2,9 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' -apply from: 'flagship-publishing.gradle' apply from: 'jacoco.gradle' + android { publishNonDefault true compileSdkVersion 33 @@ -111,3 +111,4 @@ repositories { mavenCentral() } +apply from: 'flagship-publishing.gradle' diff --git a/flagship/flagship-publishing.gradle b/flagship/flagship-publishing.gradle index 398a0b2..f0e62b4 100644 --- a/flagship/flagship-publishing.gradle +++ b/flagship/flagship-publishing.gradle @@ -5,12 +5,12 @@ project(':flagship') { publishing { publications { maven(MavenPublication) { - groupId = artifactory_group_id - artifactId = artifactory_artifact_id + groupId = maven_group_id + artifactId = maven_artifact_id version = flagship_version_name - artifact "$buildDir/outputs/aar/" + artifactory_artifact_id + "-" + flagship_version_name + ".aar" + artifact "$buildDir/outputs/aar/" + maven_artifact_id + "-" + flagship_version_name + ".aar" pom { - name = artifactory_artifact_id + name = maven_artifact_id description = 'Visit https://developers.flagship.io/ to get started with Flagship.' url = 'https://github.com/flagship-io/flagship-android' licenses { @@ -42,7 +42,7 @@ project(':flagship') { dependencyNode.appendNode('version', dependency.version) } } - if (artifactory_variant == 'compat') { + if (maven_variant == 'compat') { configurations.compatImplementation.allDependencies.each { dependency -> if (dependency.name != "unspecified") { def dependencyNode = dependenciesNode.appendNode('dependency') @@ -70,8 +70,8 @@ project(':flagship') { name = "OSSRH" url = 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/' credentials { - username = ossrhUsername - password = ossrhPassword + username = sonatype_username + password = sonatype_password } } } @@ -79,5 +79,7 @@ project(':flagship') { } signing { + if (sonatype_signing_key && sonatype_signing_pwd) + useInMemoryPgpKeys(sonatype_signing_key, sonatype_signing_pwd) sign publishing.publications.maven } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 1c34c94..a0786a4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,5 +26,6 @@ android.nonFinalResIds=false signing.keyId= signing.password= signing.secretKeyRingFile= +stagingRepositoryId= ossrhUsername= -ossrhPassword= \ No newline at end of file +ossrhPassword= From 7e30e85b5e540d67a14d29959ba3ce97e2031363 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 3 Oct 2023 17:08:21 +0200 Subject: [PATCH 15/70] up to date --- flagship/flagship-publishing.gradle | 3 --- 1 file changed, 3 deletions(-) diff --git a/flagship/flagship-publishing.gradle b/flagship/flagship-publishing.gradle index 939d9e2..f0e62b4 100644 --- a/flagship/flagship-publishing.gradle +++ b/flagship/flagship-publishing.gradle @@ -79,10 +79,7 @@ project(':flagship') { } signing { -<<<<<<< HEAD -======= if (sonatype_signing_key && sonatype_signing_pwd) useInMemoryPgpKeys(sonatype_signing_key, sonatype_signing_pwd) ->>>>>>> dcaac656f7de02036ecc9e2c9313e5a76fa8b4f9 sign publishing.publications.maven } \ No newline at end of file From 146f9e5bb63e4eb254fb904b9bfb6b052133adc5 Mon Sep 17 00:00:00 2001 From: raf-abtasty Date: Wed, 8 Nov 2023 13:57:45 +0100 Subject: [PATCH 16/70] Feature/fs3 1034 fetch warning (#6) - **Changed** - userExposed has been renamed to visitorExposed - **Added** - Flagmetadata now returns campaign name, variation group name and variation name. - onVisitorExposed callback - Warning when Flags need to be updated --- .github/workflows/ci-unitest-build.yml | 9 +- .github/workflows/release.yml | 9 +- .github/workflows/rollback.yml | 19 -- app/build.gradle | 20 +- .../ui/dashboard/ConfigViewModel.kt | 8 + .../ui/modifications/ModificationFragment.kt | 4 +- .../ui/modifications/ModificationViewModel.kt | 56 ++-- build.gradle | 19 +- flagship/build.gradle | 44 +-- flagship/proguard-rules.pro | 4 +- .../2.json | 72 +++++ .../flagship/api/IFlagshipEndpoints.kt | 2 - .../abtasty/flagship/api/TrackingManager.kt | 45 ++- .../flagship/cache/DefaultCacheManager.kt | 29 +- .../flagship/cache/VisitorCacheHelper.kt | 80 +++-- .../flagship/database/DefaultDatabase.kt | 13 +- .../abtasty/flagship/decision/ApiManager.kt | 21 +- .../flagship/decision/BucketingManager.kt | 17 +- .../flagship/decision/IDecisionManager.kt | 4 +- .../com/abtasty/flagship/hits/Activate.kt | 8 +- .../abtasty/flagship/main/FlagshipConfig.kt | 22 +- .../com/abtasty/flagship/model/Campaign.kt | 19 +- .../flagship/model/CampaignMetadata.kt | 30 ++ .../java/com/abtasty/flagship/model/Flag.kt | 62 +++- .../abtasty/flagship/model/FlagMetadata.kt | 73 ++--- .../com/abtasty/flagship/model/Variation.kt | 73 ++++- .../abtasty/flagship/model/VariationGroup.kt | 72 +++-- .../flagship/model/VariationGroupMetadata.kt | 21 ++ .../flagship/model/VariationMetadata.kt | 35 +++ .../flagship/utils/EFlagFetchingStatus.kt | 9 + .../flagship/utils/FlagshipConstants.kt | 12 +- .../flagship/utils/FlagshipLogManager.kt | 2 +- .../flagship/visitor/DefaultStrategy.kt | 140 ++++++--- .../com/abtasty/flagship/visitor/IVisitor.kt | 2 +- .../flagship/visitor/NoConsentStrategy.kt | 6 +- .../flagship/visitor/NotReadyStrategy.kt | 15 +- .../abtasty/flagship/visitor/PanicStrategy.kt | 14 +- .../com/abtasty/flagship/visitor/Visitor.kt | 7 +- .../flagship/visitor/VisitorDelegate.kt | 18 +- .../flagship/visitor/VisitorDelegateDTO.kt | 16 +- .../flagship/visitor/VisitorExposed.kt | 11 + .../flagship/visitor/VisitorStrategy.kt | 10 +- flagship/src/test/assets/api_response_1.json | 5 + .../src/test/assets/bucketing_response_1.json | 6 + .../com/abtasty/flagship/FlagshipTests.kt | 296 +++++++++++++++--- gradle/wrapper/gradle-wrapper.properties | 2 +- 46 files changed, 1055 insertions(+), 406 deletions(-) delete mode 100644 .github/workflows/rollback.yml create mode 100644 flagship/schemas/com.abtasty.flagship.database.DefaultDatabase/2.json create mode 100644 flagship/src/main/java/com/abtasty/flagship/model/CampaignMetadata.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/model/VariationGroupMetadata.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/model/VariationMetadata.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/utils/EFlagFetchingStatus.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/visitor/VisitorExposed.kt diff --git a/.github/workflows/ci-unitest-build.yml b/.github/workflows/ci-unitest-build.yml index ed6c08f..32ff40e 100644 --- a/.github/workflows/ci-unitest-build.yml +++ b/.github/workflows/ci-unitest-build.yml @@ -15,11 +15,12 @@ jobs: runs-on: ubuntu-latest if: contains(github.event.head_commit.message, '#ci-auto') == false steps: - - uses: actions/checkout@v2 - - name: set up JDK 1.11 - uses: actions/setup-java@v1 + - uses: actions/checkout@v3 + - name: set up JDK 17 + uses: actions/setup-java@v3 with: - java-version: 1.11 + distribution: "oracle" + java-version: '17' - name: Unit tests run: bash ./gradlew flagship:testAllVariantsWithCoverage - name: Upload coverage to codecov diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e019e8..57402dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,11 +10,12 @@ jobs: runs-on: ubuntu-latest if: contains(github.event.head_commit.message, '#ci-auto') == false steps: - - uses: actions/checkout@v2 - - name: set up JDK 1.11 - uses: actions/setup-java@v1 + - uses: actions/checkout@v3 + - name: set up JDK 17 + uses: actions/setup-java@v3 with: - java-version: 1.11 + distribution: "oracle" + java-version: "17" - name: Unit tests run: bash ./gradlew clean flagship:testAllVariantsWithCoverage - name: Get version diff --git a/.github/workflows/rollback.yml b/.github/workflows/rollback.yml deleted file mode 100644 index 77a940e..0000000 --- a/.github/workflows/rollback.yml +++ /dev/null @@ -1,19 +0,0 @@ - -name: Artifactory rollback CI -# -#on: -# delete: -# tags: -# - '*.*.*' -# - '*.*' -# -#jobs: -# proceed: -# runs-on: ubuntu-latest -# -# steps: -# - uses: actions/checkout@v2 -# - name: Artifactory delete version -# run: | -# curl --location --request DELETE 'https://abtasty.jfrog.io/artifactory/flagship-android/com/abtasty/flagship-android/${{ github.event.ref }}' --header 'Authorization: Bearer ${{ secrets.ARTIFACTORY_TOKEN }}' -# curl --location --request DELETE 'https://abtasty.jfrog.io/artifactory/flagship-android-compat/com/abtasty/flagship-android-compat/${{ github.event.ref }}' --header 'Authorization: Bearer ${{ secrets.ARTIFACTORY_TOKEN }}' \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index efa44e6..0820283 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ def keystoreProperties = new Properties() keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) android { - compileSdkVersion 33 + compileSdk 34 signingConfigs { release { @@ -26,7 +26,7 @@ android { applicationId "com.abtasty.flagshipqa" minSdkVersion 21 - targetSdkVersion 33 + targetSdk 34 versionCode 1 versionName "1.0" multiDexEnabled true @@ -45,11 +45,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } flavorDimensions 'default' @@ -70,14 +70,14 @@ dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.10.1' + implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.9.0' + implementation 'com.google.android.material:material:1.10.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0' - implementation 'androidx.navigation:navigation-ui-ktx:2.6.0' + implementation 'androidx.navigation:navigation-fragment-ktx:2.7.4' + implementation 'androidx.navigation:navigation-ui-ktx:2.7.4' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation "org.jetbrains.kotlin:kotlin-reflect:1.8.10" + implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.10" implementation project(path: ':flagship') //Use local project // implementation 'com.abtasty:flagship-android:3.0.5' //Use remote maven repository 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 ca22275..243a9a8 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 @@ -87,6 +87,14 @@ class ConfigViewModel(val appContext: Application) : AndroidViewModel(appContext ready() } } + flagshipConfig.withOnVisitorExposed { visitorExposed, exposedFlag -> + System.out.println("[OnVisitorExposed] : " + visitorExposed.visitorId + " \n" + + "key: " + exposedFlag.key + "\n" + + "value: " + exposedFlag.value + "\n" + + "Campaign name: " + exposedFlag.metadata.campaignName + "\n" + + "variation name: " + exposedFlag.metadata.variationName + ) + } Flagship.start(getApplication(), env_id.value!!, api_key.value!!, flagshipConfig.build()) } } diff --git a/app/src/main/java/com/abtasty/flagshipqa/ui/modifications/ModificationFragment.kt b/app/src/main/java/com/abtasty/flagshipqa/ui/modifications/ModificationFragment.kt index e5529ac..1ca1d36 100644 --- a/app/src/main/java/com/abtasty/flagshipqa/ui/modifications/ModificationFragment.kt +++ b/app/src/main/java/com/abtasty/flagshipqa/ui/modifications/ModificationFragment.kt @@ -62,7 +62,9 @@ class ModificationFragment : Fragment() { }) binding.activate.setOnClickListener { - modificationViewModel.activate(binding.editTextKey.text.toString()) + modificationViewModel.activate( binding.editTextKey.text.toString(), + binding.editTextDefault.text.toString(), + binding.spinner.selectedItem.toString()) } return binding.root 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 6020707..56335ee 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,6 +6,7 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import com.abtasty.flagship.main.Flagship import com.abtasty.flagship.model.Modification +import com.abtasty.flagship.model._Flag import com.abtasty.flagship.visitor.VisitorDelegate import org.json.JSONObject import java.util.concurrent.ConcurrentHashMap @@ -34,39 +35,41 @@ class ModificationViewModel(val appContext: Application) : AndroidViewModel(appC } fun loadModification() { - var visitorModification : ConcurrentMap = ConcurrentHashMap() + var visitorFlags: ConcurrentMap = ConcurrentHashMap() try { - visitorModification = readInstanceProperty(Flagship.getVisitor()!!, - "delegate").modifications - } catch (e : Exception) { - + visitorFlags = readInstanceProperty( + Flagship.getVisitor()!!, + "delegate" + ).flags + } catch (e: Exception) { + e.printStackTrace() } val json = JSONObject("{}") - for (e in visitorModification) { - if (e.value.value == null) - json.put(e.key, JSONObject.NULL) + for ((key, flag) in visitorFlags) { + if (flag.value == null) + json.put(key, JSONObject.NULL) else - json.put(e.key, e.value.value) + json.put(key, flag.value) } modifications.value = json.toString(4) } - fun getModification(key: String, default: String, type: String) { - value.value = when (type) { - "String" -> Flagship.getVisitor()?.getModification(key, default) - "Boolean" -> Flagship.getVisitor()?.getModification(key, default.toLowerCase().toBoolean()) + fun getTypedValue(type: String, default : String) : Any { + return when (type) { + "String" -> default + "Boolean" -> default.toLowerCase().toBoolean() "Number" -> { try { - Flagship.getVisitor()?.getModification(key, default.toInt()) + default.toInt() } catch (e: NumberFormatException) { try { - Flagship.getVisitor()?.getModification(key, default.toDouble()) + default.toDouble() } catch (e: NumberFormatException) { try { - Flagship.getVisitor()?.getModification(key, default.toFloat()) + default.toFloat() } catch (e: NumberFormatException) { try { - Flagship.getVisitor()?.getModification(key, default.toLong()) + default.toLong() } catch (e: NumberFormatException) { -1 } @@ -75,15 +78,26 @@ class ModificationViewModel(val appContext: Application) : AndroidViewModel(appC } } "Json" -> { - Flagship.getVisitor()?.getModification(key, JSONObject()).toString(); + JSONObject() } + else -> "unknown" } - info.value = Flagship.getVisitor()?.getModificationInfo(key) ?: JSONObject() } - fun activate(key : String) { - Flagship.getVisitor()?.activateModification(key) + fun getModification(key: String, default: String, type: String) { + Flagship.getVisitor()?.let { visitor -> + value.value = visitor.getModification(key, getTypedValue(type, default)) + info.value = visitor.getFlag(key, getTypedValue(type, default)).metadata().toJson() + } + + } + + fun activate(key: String, default: String, type: String) { +// Flagship.getVisitor()?.activateModification(key) + Flagship.getVisitor()?.let { visitor -> + visitor.getFlag(key, getTypedValue(type, default)).visitorExposed() + } Toast.makeText(appContext, "Activation sent", Toast.LENGTH_SHORT).show(); } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 50f20da..bf0f4e1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,5 @@ -apply plugin: 'io.github.gradle-nexus.publish-plugin' + + buildscript { /* @@ -7,7 +8,7 @@ buildscript { */ ext { - kotlin_version = '1.8.20' + kotlin_version = '1.9.10' maven_artifact_id = "flagship-android" maven_repo = "flagship-android" maven_group_id = "com.abtasty" @@ -16,8 +17,8 @@ buildscript { if (!maven_variant.isEmpty()) { maven_artifact_id = maven_artifact_id + '-' + maven_variant } - flagship_version_name = System.getenv('FLAGSHIP_VERSION_NAME') ?: "3.0.6" - flagship_version_code = System.getenv('FLAGSHIP_VERSION_CODE') ?: 15 + flagship_version_name = System.getenv('FLAGSHIP_VERSION_NAME') ?: "3.1.0" + flagship_version_code = System.getenv('FLAGSHIP_VERSION_CODE') ?: 17 sonatype_signing_key = System.getenv('SONATYPE_SIGNING_KEY') sonatype_signing_pwd = System.getenv('SONATYPE_SIGNING_PWD') sonatype_username = System.getenv('SONATYPE_USERNAME') ?: ossrhUsername @@ -28,17 +29,21 @@ buildscript { repositories { google() mavenCentral() - maven { url 'https://jitpack.io' } } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.24.18" classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.16" 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 +} + +apply plugin: 'io.github.gradle-nexus.publish-plugin' + allprojects { repositories { google() diff --git a/flagship/build.gradle b/flagship/build.gradle index b0fade0..af07857 100644 --- a/flagship/build.gradle +++ b/flagship/build.gradle @@ -1,23 +1,24 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-parcelize' -apply plugin: 'kotlin-kapt' -apply from: 'jacoco.gradle' +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'kotlin-parcelize' + id 'com.google.devtools.ksp' +} + +apply from: 'jacoco.gradle' android { publishNonDefault true - compileSdkVersion 33 + compileSdk 34 defaultConfig { minSdkVersion 21 - compileSdkVersion 33 - targetSdkVersion 33 + compileSdk 34 + targetSdkVersion 34 - kapt { - arguments { - arg("room.schemaLocation", "$projectDir/schemas".toString()) - } + ksp { + arg("room.schemaLocation", "$projectDir/schemas".toString()) } } @@ -41,13 +42,16 @@ android { resValue("string", "ABTastyVersion", "${flagship_version_name}") } jacoco { - version = '0.8.7' + version = '0.8.11' } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' } android.libraryVariants.all { variant -> @@ -87,13 +91,13 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'androidx.test:core:1.5.0' testImplementation 'org.robolectric:robolectric:4.10' - testImplementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' + testImplementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - testCompatImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.2' + testCompatImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.2' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" - implementation 'androidx.room:room-runtime:2.5.1' + implementation 'androidx.room:room-runtime:2.6.0' commonImplementation 'com.squareup.okhttp3:okhttp:4.9.3' @@ -101,8 +105,8 @@ dependencies { compatImplementation 'com.squareup.okhttp3:okhttp:3.12.12' compatImplementation 'org.conscrypt:conscrypt-android:2.5.1' - annotationProcessor("androidx.room:room-compiler:2.5.1") - kapt 'androidx.room:room-compiler:2.5.1' + annotationProcessor("androidx.room:room-compiler:2.6.0") + ksp 'androidx.room:room-compiler:2.6.0' } diff --git a/flagship/proguard-rules.pro b/flagship/proguard-rules.pro index eba9010..1744ef8 100644 --- a/flagship/proguard-rules.pro +++ b/flagship/proguard-rules.pro @@ -61,4 +61,6 @@ # -keep public class mypackage.MyClass # -keep public interface mypackage.MyInterface -# -keep public class * implements mypackage.MyInterface \ No newline at end of file +# -keep public class * implements mypackage.MyInterface + +-dontwarn java.lang.invoke.StringConcatFactory \ No newline at end of file diff --git a/flagship/schemas/com.abtasty.flagship.database.DefaultDatabase/2.json b/flagship/schemas/com.abtasty.flagship.database.DefaultDatabase/2.json new file mode 100644 index 0000000..3118d5a --- /dev/null +++ b/flagship/schemas/com.abtasty.flagship.database.DefaultDatabase/2.json @@ -0,0 +1,72 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "153c86145807fbdbf19c1317e3551b74", + "entities": [ + { + "tableName": "visitors", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`visitorId` TEXT NOT NULL, `data` TEXT NOT NULL, PRIMARY KEY(`visitorId`))", + "fields": [ + { + "fieldPath": "visitorId", + "columnName": "visitorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "visitorId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "hits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`visitorId` TEXT NOT NULL, `data` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "visitorId", + "columnName": "visitorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '153c86145807fbdbf19c1317e3551b74')" + ] + } +} \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/api/IFlagshipEndpoints.kt b/flagship/src/main/java/com/abtasty/flagship/api/IFlagshipEndpoints.kt index 2eb7aa3..a155192 100644 --- a/flagship/src/main/java/com/abtasty/flagship/api/IFlagshipEndpoints.kt +++ b/flagship/src/main/java/com/abtasty/flagship/api/IFlagshipEndpoints.kt @@ -6,8 +6,6 @@ class IFlagshipEndpoints { val DECISION_API = "https://decision.flagship.io/v2/" val BUCKETING = "https://cdn.flagship.io/%s/bucketing.json" val CAMPAIGNS = "/campaigns/?exposeAllKeys=true" // call to /event not needed in api mode - val CONTEXT_PARAM = "&sendContextEvent=false" - // String CAMPAIGNS = "/campaigns/?exposeAllKeys=true&sendContextEvent=false"; // call to /event needed in api mode val ARIANE = "https://ariane.abtasty.com" val ACTIVATION = "activate" val EVENTS = "/events" 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 774f59e..5f86009 100644 --- a/flagship/src/main/java/com/abtasty/flagship/api/TrackingManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/api/TrackingManager.kt @@ -14,12 +14,14 @@ import com.abtasty.flagship.utils.FlagshipConstants import com.abtasty.flagship.utils.FlagshipLogManager import com.abtasty.flagship.utils.LogManager import com.abtasty.flagship.visitor.VisitorDelegateDTO +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async import kotlinx.coroutines.launch import org.json.JSONObject class TrackingManager { - private fun sendActivation(visitor: VisitorDelegateDTO, hit: Activate) { + private fun sendActivation(visitor: VisitorDelegateDTO, hit: Activate): Deferred { val headers: HashMap = HashMap() headers["x-sdk-client"] = "android" headers["x-sdk-version"] = BuildConfig.FLAGSHIP_VERSION_NAME @@ -34,12 +36,14 @@ class TrackingManager { data.put(FlagshipConstants.HitKeyMap.VISITOR_ID, visitor.anonymousId) data.put(FlagshipConstants.HitKeyMap.ANONYMOUS_ID, JSONObject.NULL) } - sendTracking(visitor, hit.type.name, DECISION_API + ACTIVATION, headers, data) + return sendTracking(visitor, hit.type.name, DECISION_API + ACTIVATION, headers, data) } - fun sendHit(visitor: VisitorDelegateDTO, hit: Hit<*>) { + fun sendHit(visitor: VisitorDelegateDTO, hit: Hit<*>): Deferred? { - if (hit is Activate) sendActivation(visitor, hit) else { + if (hit is Activate) + return sendActivation(visitor, hit) + else { if (hit.checkData()) { val data = hit.data if (visitor.visitorId.isNotEmpty() && visitor.anonymousId != null) { @@ -52,11 +56,12 @@ class TrackingManager { data.put(FlagshipConstants.HitKeyMap.VISITOR_ID, visitor.anonymousId) data.put(FlagshipConstants.HitKeyMap.CUSTOM_VISITOR_ID, JSONObject.NULL) } - sendTracking(visitor, hit.type.name, ARIANE, null, data) + return sendTracking(visitor, hit.type.name, ARIANE, null, data) } else FlagshipLogManager.log(FlagshipLogManager.Tag.TRACKING, LogManager.Level.ERROR, String.format(FlagshipConstants.Errors.HIT_INVALID_DATA_ERROR, hit.type, hit) ) } + return null } fun sendContextRequest(visitor: VisitorDelegateDTO) { @@ -79,16 +84,32 @@ class TrackingManager { } } - private fun sendTracking(visitorDTO: VisitorDelegateDTO, type: String, endPoint : String, headers: HashMap? = null, data : JSONObject, time : Long = -1) { - Flagship.coroutineScope().launch { - val response = HttpManager.sendAsyncHttpRequest(HttpManager.RequestType.POST, - endPoint, headers, data.toString()).await() - val tag = if (type == FlagshipLogManager.Tag.ACTIVATE.name) FlagshipLogManager.Tag.ACTIVATE else FlagshipLogManager.Tag.TRACKING + private fun sendTracking( + visitorDTO: VisitorDelegateDTO, + type: String, + endPoint: String, + headers: HashMap? = null, + data: JSONObject, + time: Long = -1 + ): Deferred { + return Flagship.coroutineScope().async { + val response = HttpManager.sendAsyncHttpRequest( + HttpManager.RequestType.POST, + endPoint, headers, data.toString() + ).await() + val tag = + if (type == FlagshipLogManager.Tag.ACTIVATE.name) FlagshipLogManager.Tag.ACTIVATE else FlagshipLogManager.Tag.TRACKING logHit(tag, response, response?.requestContent) if (response == null || response.code !in 200..204) { val json = HitCacheHelper.fromHit(visitorDTO, type, data, time) - visitorDTO.visitorStrategy.cacheHit(visitorDTO.visitorId, json) - } + try { + visitorDTO.visitorStrategy.cacheHit(visitorDTO.visitorId, json) + } catch (e: Exception) { + FlagshipLogManager.exception(e) + } + false + } else + true } } 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 22dafe0..48b2624 100644 --- a/flagship/src/main/java/com/abtasty/flagship/cache/DefaultCacheManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/cache/DefaultCacheManager.kt @@ -1,6 +1,9 @@ package com.abtasty.flagship.cache import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase import com.abtasty.flagship.database.DefaultDatabase import com.abtasty.flagship.database.Hit import com.abtasty.flagship.database.Visitor @@ -17,16 +20,28 @@ import org.json.JSONObject class DefaultCacheManager : CacheManager() { private val db: DefaultDatabase by lazy { - Room.databaseBuilder(Flagship.application, DefaultDatabase::class.java, "flagship-cache").build() + Room.databaseBuilder(Flagship.application, DefaultDatabase::class.java, "flagship-cache") + .addCallback(object : RoomDatabase.Callback() { + override fun onOpen(db: SupportSQLiteDatabase) { + super.onOpen(db) + db.execSQL("CREATE TEMP TABLE IF NOT EXISTS room_table_modification_log(table_id INTEGER PRIMARY KEY, invalidated INTEGER NOT NULL DEFAULT 0)") + } + }) + .build() } - override var visitorCacheImplementation: IVisitorCacheImplementation? = object : IVisitorCacheImplementation { + override var visitorCacheImplementation: IVisitorCacheImplementation? = + object : IVisitorCacheImplementation { - override fun cacheVisitor(visitorId: String, data: JSONObject) { - db.visitorDao().upsert(Visitor(visitorId, data.toString())) - FlagshipLogManager.log(FlagshipLogManager.Tag.DEFAULT_CACHE_MANAGER, LogManager.Level.INFO, - FlagshipConstants.Info.DEFAULT_CACHE_MANAGER_CACHE_VISITOR.format(visitorId, data.toString(4))) - } + override fun cacheVisitor(visitorId: String, data: JSONObject) { + db.visitorDao().upsert(Visitor(visitorId, data.toString())) + FlagshipLogManager.log( + FlagshipLogManager.Tag.DEFAULT_CACHE_MANAGER, LogManager.Level.INFO, + FlagshipConstants.Info.DEFAULT_CACHE_MANAGER_CACHE_VISITOR.format( + visitorId, + data.toString(4) + )) + } override fun lookupVisitor(visitorId: String): JSONObject { var result = JSONObject() 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 bdd69bd..aae8399 100644 --- a/flagship/src/main/java/com/abtasty/flagship/cache/VisitorCacheHelper.kt +++ b/flagship/src/main/java/com/abtasty/flagship/cache/VisitorCacheHelper.kt @@ -1,6 +1,11 @@ package com.abtasty.flagship.cache +import com.abtasty.flagship.model.CampaignMetadata +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 import com.abtasty.flagship.model.iterator import com.abtasty.flagship.utils.FlagshipConstants import com.abtasty.flagship.utils.FlagshipLogManager @@ -25,7 +30,8 @@ class VisitorCacheHelper: CacheHelper() { val visitorDelegate = visitorDelegateDTO.visitorDelegate if (dataObject.getString("visitorId") == visitorDelegate.visitorId) { // todo think anonymous visitorDelegate.visitorId = dataObject.optString("visitorId") - visitorDelegate.anonymousId = dataObject.optString("anonymousId", null) + if (dataObject.has("anonymousId")) + visitorDelegate.anonymousId = dataObject.getString("anonymousId") visitorDelegate.hasConsented = dataObject.optBoolean("consent", true) dataObject.optJSONObject("context")?.let { for (k in it.keys()) { @@ -41,17 +47,25 @@ class VisitorCacheHelper: CacheHelper() { visitorDelegate.activatedVariations.add(campaignJSON.getString("variationId")) campaignJSON.optJSONObject("flags")?.let { flagJSON -> for (k in flagJSON.keys()) { - val modification = Modification( - k, - campaignJSON.getString("campaignId"), - campaignJSON.getString("variationGroupId"), - campaignJSON.getString("variationId"), - campaignJSON.getBoolean("isReference"), - flagJSON.get(k), - campaignJSON.getString("type"), - campaignJSON.optString("slug", "") + val metadata = FlagMetadata( + VariationMetadata( + campaignJSON.getString("variationId"), + campaignJSON.optString("variationName"), + campaignJSON.getBoolean("isReference"), + campaignJSON.optInt("allocation", 0), + VariationGroupMetadata( + campaignJSON.getString("variationGroupId"), + campaignJSON.optString("variationGroupName"), + CampaignMetadata( + campaignJSON.getString("campaignId"), + campaignJSON.optString("campaignName"), + campaignJSON.getString("type"), + campaignJSON.optString("slug") + ) + ) + ) ) - visitorDelegate.modifications[k] = modification + visitorDelegate.flags[k] = _Flag(k, flagJSON.get(k), metadata) } } } @@ -75,37 +89,46 @@ class VisitorCacheHelper: CacheHelper() { .put("anonymousId", visitorDelegateDTO.anonymousId) .put("consent", visitorDelegateDTO.hasConsented) .put("context", visitorDelegateDTO.contextToJson()) - .put("campaigns", modificationsToCacheJSON(visitorDelegateDTO)) + .put("campaigns", flagsToCacheJSON(visitorDelegateDTO)) .put("assignmentsHistory", assignationHistoryToCacheJSON(visitorDelegateDTO)) return JSONObject() .put("version", _VISITOR_CACHE_VERSION_) .put("data", data) } - private fun modificationsToCacheJSON(visitorDelegateDTO: VisitorDelegateDTO): JSONArray { + private fun flagsToCacheJSON(visitorDelegateDTO: VisitorDelegateDTO): JSONArray { val campaigns = JSONArray() - for (m in visitorDelegateDTO.modifications) { + for ((key, flag) in visitorDelegateDTO.flags) { var isCampaignSet = false for (i in 0 until campaigns.length()) { val campaign = campaigns.getJSONObject(i) - if (campaign.optString("campaignId") == m.value.campaignId && campaign.optString("variationGroupId") == m.value.variationGroupId && - campaign.optString("variationId") == m.value.variationId + if (campaign.optString("campaignId") == flag.metadata.campaignId && campaign.optString( + "variationGroupId" + ) == flag.metadata.variationGroupId && + campaign.optString("variationId") == flag.metadata.variationId ) { isCampaignSet = true - campaign.optJSONObject("flags")?.put(m.value.key, m.value.value ?: JSONObject.NULL) + campaign.optJSONObject("flags")?.put(key, flag.value ?: JSONObject.NULL) } } if (!isCampaignSet) { - campaigns.put(JSONObject() - .put("campaignId", m.value.campaignId) - .put("variationGroupId", m.value.variationGroupId) - .put("variationId", m.value.variationId) - .put("isReference", m.value.isReference) - .put("type", m.value.campaignType) - .put("slug", m.value.slug) - .put("activated", visitorDelegateDTO.activatedVariations.contains(m.value.variationId)) - .put("flags", JSONObject().put(m.value.key, m.value.value ?: JSONObject.NULL)) + campaigns.put( + JSONObject() + .put("campaignId", flag.metadata.campaignId) + .put("campaignName", flag.metadata.campaignName) + .put("variationGroupId", flag.metadata.variationGroupId) + .put("variationGroupName", flag.metadata.variationGroupName) + .put("variationId", flag.metadata.variationId) + .put("variationName", flag.metadata.variationName) + .put("isReference", flag.metadata.isReference) + .put("type", flag.metadata.campaignType) + .put("slug", flag.metadata.slug) + .put( + "activated", + visitorDelegateDTO.activatedVariations.contains(flag.metadata.variationId) + ) + .put("flags", JSONObject().put(key, flag.value ?: JSONObject.NULL)) ) } } @@ -114,11 +137,6 @@ class VisitorCacheHelper: CacheHelper() { @Suppress("unchecked_cast") private fun assignationHistoryToCacheJSON(visitorDelegateDTO: VisitorDelegateDTO): JSONObject { -// val assignationsJSON = JSONObject() -// for ((key, value) in visitorDelegateDTO.assignmentsHistory) { -// assignationsJSON.put(key, value) -// } -// return assignationsJSON return JSONObject(visitorDelegateDTO.assignmentsHistory as Map) } diff --git a/flagship/src/main/java/com/abtasty/flagship/database/DefaultDatabase.kt b/flagship/src/main/java/com/abtasty/flagship/database/DefaultDatabase.kt index e9b17ea..1ce34f7 100644 --- a/flagship/src/main/java/com/abtasty/flagship/database/DefaultDatabase.kt +++ b/flagship/src/main/java/com/abtasty/flagship/database/DefaultDatabase.kt @@ -1,10 +1,17 @@ package com.abtasty.flagship.database +import androidx.room.AutoMigration import androidx.room.Database import androidx.room.RoomDatabase -@Database(entities = [Visitor::class, Hit::class], version = 1) +@Database( + entities = [Visitor::class, Hit::class], version = 2, autoMigrations = [ + AutoMigration( + from = 1, + to = 2 + )], exportSchema = true +) abstract class DefaultDatabase : RoomDatabase() { - abstract fun visitorDao() : VisitorDao - abstract fun hitDao() : HitDao + abstract fun visitorDao(): VisitorDao + abstract fun hitDao(): HitDao } \ No newline at end of file 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 d4681d9..a65cd7e 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/ApiManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/ApiManager.kt @@ -3,7 +3,7 @@ package com.abtasty.flagship.decision import com.abtasty.flagship.BuildConfig import com.abtasty.flagship.api.HttpManager import com.abtasty.flagship.api.IFlagshipEndpoints.Companion.CAMPAIGNS -import com.abtasty.flagship.api.IFlagshipEndpoints.Companion.CONTEXT_PARAM +//import com.abtasty.flagship.api.IFlagshipEndpoints.Companion.CONTEXT_PARAM import com.abtasty.flagship.api.IFlagshipEndpoints.Companion.DECISION_API import com.abtasty.flagship.api.ResponseCompat import com.abtasty.flagship.main.Flagship @@ -11,6 +11,7 @@ import com.abtasty.flagship.main.Flagship.getStatus import com.abtasty.flagship.main.FlagshipConfig import com.abtasty.flagship.model.Campaign import com.abtasty.flagship.model.Modification +import com.abtasty.flagship.model._Flag import com.abtasty.flagship.utils.FlagshipLogManager import com.abtasty.flagship.utils.LogManager import com.abtasty.flagship.visitor.VisitorDelegateDTO @@ -34,32 +35,32 @@ class ApiManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flagshipCo json.put("visitorId", visitorDelegateDTO.visitorId) json.put("anonymousId", visitorDelegateDTO.anonymousId) json.put("trigger_hit", false) + json.put("visitor_consent", visitorDelegateDTO.hasConsented) json.put("context", visitorDelegateDTO.contextToJson()) val response: ResponseCompat = HttpManager.sendHttpRequest(HttpManager.RequestType.POST, - DECISION_API + flagshipConfig.envId + CAMPAIGNS + if (!visitorDelegateDTO.hasConsented) CONTEXT_PARAM else "", headers, json.toString()) + DECISION_API + flagshipConfig.envId + CAMPAIGNS, headers, json.toString()) logResponse(response) val results = if (response.code < 400) parseCampaignsResponse(response.content) else null updateFlagshipStatus(if (panic) Flagship.Status.PANIC else Flagship.Status.READY) return results } - - override fun getCampaignsModifications(visitorDelegateDTO : VisitorDelegateDTO): HashMap? { - val campaignsModifications: HashMap = HashMap() + override fun getCampaignFlags(visitorDelegateDTO : VisitorDelegateDTO): HashMap? { + val campaignsFlags: HashMap = HashMap() try { sendCampaignRequest(visitorDelegateDTO)?.let { campaigns -> - for ((_, _, variationGroups) in campaigns) { + for ((_, variationGroups) in campaigns) { for (variationGroup in variationGroups) { for (variation in variationGroup?.variations!!.values) { - visitorDelegateDTO.addNewAssignmentToHistory(variation.variationGroupId, variation.variationId); //save for cache - variation.getModificationsValues()?.let { modificationsValues -> - campaignsModifications.putAll(modificationsValues) + visitorDelegateDTO.addNewAssignmentToHistory(variation.variationMetadata.variationGroupId, variation.variationMetadata.variationId); //save for cache + variation.flags?.let { flags -> + campaignsFlags.putAll(flags) } } } } } - return campaignsModifications + return campaignsFlags } catch (e: Exception) { FlagshipLogManager.log(FlagshipLogManager.Tag.FLAGS_FETCH, LogManager.Level.ERROR, FlagshipLogManager.exceptionToString(e) ?: "") } 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 1d90530..9c9764b 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/BucketingManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/BucketingManager.kt @@ -8,6 +8,7 @@ import com.abtasty.flagship.main.Flagship.getStatus import com.abtasty.flagship.main.FlagshipConfig import com.abtasty.flagship.model.Campaign import com.abtasty.flagship.model.Modification +import com.abtasty.flagship.model._Flag import com.abtasty.flagship.utils.FlagshipConstants import com.abtasty.flagship.utils.FlagshipConstants.Errors.Companion.BUCKETING_POLLING_ERROR import com.abtasty.flagship.utils.FlagshipLogManager @@ -106,25 +107,25 @@ class BucketingManager(flagshipConfig: FlagshipConfig<*>) : DecisionManager(flag executor = null } - override fun getCampaignsModifications(visitorDelegateDTO: VisitorDelegateDTO): HashMap? { - val campaignsModifications: HashMap = HashMap() + override fun getCampaignFlags(visitorDelegateDTO: VisitorDelegateDTO): HashMap? { + val campaignsFlags: HashMap = HashMap() try { - for ((_, _, variationGroups) in campaigns) { + for ((_, variationGroups) in campaigns) { for (variationGroup in variationGroups) { if (variationGroup!!.isTargetingValid(HashMap(visitorDelegateDTO.context))) { val variation = variationGroup.selectVariation(visitorDelegateDTO) if (variation != null) { - visitorDelegateDTO.addNewAssignmentToHistory(variation.variationGroupId, variation.variationId) - val modificationsValues = variation.getModificationsValues() - if (modificationsValues != null) - campaignsModifications.putAll(modificationsValues) + visitorDelegateDTO.addNewAssignmentToHistory(variation.variationMetadata.variationGroupId, variation.variationMetadata.variationId) + variation.flags?.let { flags -> + campaignsFlags.putAll(flags) + } break } } } } visitorDelegateDTO.visitorStrategy.sendContextRequest() - return campaignsModifications + return campaignsFlags } catch (e: Exception) { FlagshipLogManager.log(FlagshipLogManager.Tag.FLAGS_FETCH, LogManager.Level.ERROR, FlagshipLogManager.exceptionToString(e) ?: "") } 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 661cfaf..b389cdc 100644 --- a/flagship/src/main/java/com/abtasty/flagship/decision/IDecisionManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/decision/IDecisionManager.kt @@ -1,8 +1,10 @@ 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 interface IDecisionManager { - fun getCampaignsModifications(visitorDelegateDTO: VisitorDelegateDTO) : HashMap? + fun getCampaignFlags(visitorDelegateDTO: VisitorDelegateDTO): HashMap? } \ 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 e2843cc..95d3f32 100644 --- a/flagship/src/main/java/com/abtasty/flagship/hits/Activate.kt +++ b/flagship/src/main/java/com/abtasty/flagship/hits/Activate.kt @@ -1,16 +1,18 @@ package com.abtasty.flagship.hits +import com.abtasty.flagship.model.FlagMetadata import com.abtasty.flagship.model.Modification +import com.abtasty.flagship.model._Flag import com.abtasty.flagship.utils.FlagshipConstants /** * Internal Hit for activations */ -class Activate(modification : Modification) : Hit(Companion.Type.ACTIVATION) { +class Activate(flagMetadata: FlagMetadata) : Hit(Companion.Type.ACTIVATION) { init { - this.data.put(FlagshipConstants.HitKeyMap.VARIATION_GROUP_ID, modification.variationGroupId) - this.data.put(FlagshipConstants.HitKeyMap.VARIATION_ID, modification.variationId) + this.data.put(FlagshipConstants.HitKeyMap.VARIATION_GROUP_ID, flagMetadata.variationGroupId) + this.data.put(FlagshipConstants.HitKeyMap.VARIATION_ID, flagMetadata.variationId) } override fun checkData(): Boolean { diff --git a/flagship/src/main/java/com/abtasty/flagship/main/FlagshipConfig.kt b/flagship/src/main/java/com/abtasty/flagship/main/FlagshipConfig.kt index 3429006..dacd1ec 100644 --- a/flagship/src/main/java/com/abtasty/flagship/main/FlagshipConfig.kt +++ b/flagship/src/main/java/com/abtasty/flagship/main/FlagshipConfig.kt @@ -2,8 +2,10 @@ package com.abtasty.flagship.main import com.abtasty.flagship.cache.CacheManager import com.abtasty.flagship.cache.DefaultCacheManager +import com.abtasty.flagship.model.ExposedFlag import com.abtasty.flagship.utils.FlagshipLogManager import com.abtasty.flagship.utils.LogManager +import com.abtasty.flagship.visitor.VisitorExposed import java.util.concurrent.TimeUnit /** @@ -20,6 +22,7 @@ abstract class FlagshipConfig(internal var decisionMode: Flagship.DecisionMod internal var pollingUnit: TimeUnit = TimeUnit.SECONDS internal var statusListener: ((Flagship.Status) -> Unit)? = null internal var cacheManager : CacheManager = DefaultCacheManager() + internal var onVisitorExposed : ((VisitorExposed, ExposedFlag<*>) -> (Unit))? = null /** * Specify the environment id provided by Flagship, to use. @@ -115,15 +118,16 @@ abstract class FlagshipConfig(internal var decisionMode: Flagship.DecisionMod return this as T } -// /** -// * Specify the Android application context in order to automatically fill device information for each Visitor Context. -// */ -// @Suppress("UNCHECKED_CAST") -// fun withApplicationContext(applicationContext : Context) : T { -// deviceContext.clear() -// deviceContext.putAll(FlagshipContext.loadAndroidContext(applicationContext)) -// return this as T -// } + /** + * Provide a code block to execute each time a Visitor is exposed to a flag. This is useful when you need to send this information to a third-party tool. + * + * @param onVisitorExposed lambda code block to execute. (VisitorExposed, ExposedFlag<*>) -> (Void) + */ + @Suppress("UNCHECKED_CAST") + fun withOnVisitorExposed(onVisitorExposed: ((VisitorExposed, ExposedFlag<*>) -> (Unit))): T { + this.onVisitorExposed = onVisitorExposed + return this as T + } fun build(): FlagshipConfig { return this 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 198d60c..f652954 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/Campaign.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/Campaign.kt @@ -10,7 +10,7 @@ import java.util.* operator fun JSONArray.iterator(): Iterator = (0 until length()).asSequence().map { get(it) as JSONObject }.iterator() -data class Campaign(val id: String, val type: String = "", val variationGroups: LinkedList) { +data class Campaign(val campaignMetadata: CampaignMetadata, val variationGroups: LinkedList) { companion object { fun parse(campaignsArray: JSONArray): ArrayList? { @@ -34,25 +34,28 @@ data class Campaign(val id: String, val type: String = "", val variationGroups: fun parse(campaignObject: JSONObject): Campaign? { return try { - val id = campaignObject.getString("id") - val type = campaignObject.optString("type") ?: "" - val slug = if (campaignObject.isNull("slug")) "" else campaignObject.optString("slug", "") + val campaignMetadata = CampaignMetadata( + campaignObject.getString("id"), + campaignObject.optString("name", ""), + campaignObject.optString("type", ""), + if (campaignObject.isNull("slug")) "" else campaignObject.optString("slug", "") + ) val variationGroups: LinkedList = LinkedList() val variationGroupArray = campaignObject.optJSONArray("variationGroups") variationGroupArray?.let { //bucketing for (variationGroupsObj in variationGroupArray) { val variationGroup: VariationGroup? = - VariationGroup.parse(id, type, slug, variationGroupsObj, true) + VariationGroup.parse(variationGroupsObj, true, campaignMetadata) variationGroup?.let { variationGroups.add(variationGroup) } } } ?: run { //api val variationGroup: VariationGroup? = - VariationGroup.parse(id, type, slug, campaignObject, false) + VariationGroup.parse(campaignObject, false, campaignMetadata) variationGroup?.let { variationGroups.add(variationGroup) } } - Campaign(id, type, variationGroups) + Campaign(campaignMetadata, variationGroups) } catch (e: Exception) { FlagshipLogManager.log( FlagshipLogManager.Tag.PARSING, @@ -65,6 +68,6 @@ data class Campaign(val id: String, val type: String = "", val variationGroups: } override fun toString(): String { - return "Campaign(id='$id', variationGroups=$variationGroups)" + 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 new file mode 100644 index 0000000..a5e6450 --- /dev/null +++ b/flagship/src/main/java/com/abtasty/flagship/model/CampaignMetadata.kt @@ -0,0 +1,30 @@ +package com.abtasty.flagship.model + +open class CampaignMetadata( + /** + * Flag use case id. + */ + val campaignId: String = "", + + /** + * Flag use case name. + */ + val campaignName: String = "", + + /** + * Flag use case type + */ + val campaignType: String = "", + + /** + * Flag use case custom slug + */ + val slug: String = "", +) { + constructor(campaignMetadata: CampaignMetadata) : this( + campaignMetadata.campaignId, + campaignMetadata.campaignName, + campaignMetadata.campaignType, + campaignMetadata.slug + ) +} \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/model/Flag.kt b/flagship/src/main/java/com/abtasty/flagship/model/Flag.kt index b511833..258f8b0 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/Flag.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/Flag.kt @@ -2,10 +2,30 @@ package com.abtasty.flagship.model import com.abtasty.flagship.visitor.VisitorDelegate +/** + * Class representing an internal Flagship flag. + */ +open class _Flag(open val key: String, open val value: Any?, open val metadata: FlagMetadata) + +/** + * Class representing a Flagship flag that has been exposed to a visitor. + */ +class ExposedFlag( + override val key: String, + override val value: T?, + val defaultValue: T?, + override val metadata: FlagMetadata +) : _Flag(key, value, metadata) { + +} + /** * Class representing a Flagship flag. */ -data class Flag(private val visitorDelegate : VisitorDelegate, val key: String, val defaultValue: T?) { +class Flag( + val visitor: VisitorDelegate, + val key: String, + val defaultValue: T? = null) { /** * Check if this flag exists in Flagship SDK. @@ -14,32 +34,48 @@ data class Flag(private val visitorDelegate : VisitorDelegate, val key return metadata().exists() } + /** * Return the current value for this flag or return default value if the flag doesn't exist or if the current value and defaultValue types are different. * - * @param userExposed Tells Flagship the user have been exposed and have seen this flag. This will increment the visits for the current variation + * @param visitorExposed Tells Flagship the user have been exposed and have seen this flag. This will increment the visits for the current variation * on your campaign reporting. Default value is true. If needed it is possible to set this param to false and call userExposed() afterward when the user sees it. */ - fun value(userExposed: Boolean = true): T? { - val value = visitorDelegate.getStrategy().getFlagValue(key, defaultValue) - if (userExposed) - userExposed() + fun value(visitorExposed: Boolean = true): T? { + val value = visitor.getStrategy().getVisitorFlagValue(key, defaultValue) + if (visitorExposed) + visitorExposed() return value } /** - * Return the campaign information metadata or an empty object if the flag doesn't exist. + * Return the campaign information metadata or an Empty flag metadata object if the flag doesn't exist. * */ - fun metadata() : FlagMetadata { - return FlagMetadata.fromModification(visitorDelegate.getStrategy().getFlagMetadata(key, defaultValue)) + fun metadata(): FlagMetadata { + return visitor.getStrategy().getVisitorFlagMetadata(key, defaultValue) + ?: FlagMetadata.EmptyFlagMetadata() } /** - * Tells Flagship the user have been exposed and have seen this flag. This will increment the visits for the current variation - * on your campaign reporting. - */ + * Tells Flagship the user have been exposed and have seen this flag. This will increment the visits for the current variation + * on your campaign reporting. + */ + @Deprecated("userExposed()", ReplaceWith("visitorExposed()")) fun userExposed() { - visitorDelegate.getStrategy().exposeFlag(key, defaultValue) + this.visitorExposed() + } + + /** + * Tells Flagship the user have been exposed and have seen this flag. This will increment the visits for the current variation + * on your campaign reporting. + */ + fun visitorExposed() { + if (exists()) + visitor.getStrategy().sendVisitorExposition(key, defaultValue) + } + + override fun toString(): String { + return "Flag(key='$key', defaultValue=$defaultValue, metadata=${metadata()})" } } \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/model/FlagMetadata.kt b/flagship/src/main/java/com/abtasty/flagship/model/FlagMetadata.kt index af49bcc..f6570fe 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/FlagMetadata.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/FlagMetadata.kt @@ -5,69 +5,50 @@ import org.json.JSONObject /** * This class contains the flag campaign information. */ -data class FlagMetadata( - /** - * Flag use case id. - */ - val campaignId: String = "", - /** - * Flag use case variation group id. - */ - val variationGroupId: String = "", - /** - * Flag use case variation id. - */ - val variationId: String = "", - /** - * Is flag from the reference variation. - */ - val isReference: Boolean = false, - /** - * Flag use case type - */ - val campaignType: String = "", - /** - * Flag use case custom slug - */ - val slug: String = "" -) { - - companion object { - internal fun fromModification(modification: Modification?): FlagMetadata { - return if (modification == null) - FlagMetadata() - else - FlagMetadata( - modification.campaignId, - modification.variationGroupId, - modification.variationId, - modification.isReference, - modification.campaignType, - modification.slug - ) - } - } +open class FlagMetadata(variationMetadata: VariationMetadata) : VariationMetadata(variationMetadata) { /** * Check if this flag metadata exists in Flagship SDK. */ - fun exists() : Boolean { + fun exists(): Boolean { return (campaignId.isNotEmpty() && variationGroupId.isNotEmpty() && variationId.isNotEmpty()) } /** * Transform the current class in json object */ - fun toJson() : JSONObject { + fun toJson(): JSONObject { return if (!exists()) JSONObject() else JSONObject() .put("campaignId", campaignId) + .put("campaignName", campaignName) + .put("campaignType", campaignType) + .put("slug", slug) .put("variationGroupId", variationGroupId) + .put("variationGroupName", variationGroupName) .put("variationId", variationId) + .put("variationName", variationName) .put("isReference", isReference) - .put("campaignType", campaignType) - .put("slug", slug) } + + class EmptyFlagMetadata() : FlagMetadata( + VariationMetadata( + "", + "", + false, + 0, + VariationGroupMetadata( + "", + "", + CampaignMetadata( + "", + "", + "", + "" + ) + ) + ) + ) } \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/model/Variation.kt b/flagship/src/main/java/com/abtasty/flagship/model/Variation.kt index 33a4696..aa6e9e1 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/Variation.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/Variation.kt @@ -3,39 +3,78 @@ 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 Variation(val campaignId : String, val variationGroupId : String, val variationId : String, - val isReference : Boolean, val modifications : Modifications?, val allocation : Int = 100) { +data class Variation( + val flags: HashMap?, + val variationMetadata: VariationMetadata +) { companion object { - fun parse(bucketingMode : Boolean, campaignId: String, campaignType: String, slug: String, variationGroupId: String, variationObj: JSONObject): Variation? { + fun parse( + variationObj: JSONObject, + bucketingMode: Boolean, + variationGroupMetadata: VariationGroupMetadata + ): Variation? { return try { - val variationId = variationObj.getString("id") - val isReference = variationObj.optBoolean("reference", false) - val modifications: Modifications? = Modifications.parse(campaignId, campaignType, slug, variationGroupId, - variationId, isReference, variationObj.getJSONObject("modifications")) - val allocation = variationObj.optInt("allocation", if (bucketingMode) 0 else 100) + val variationMetadata = VariationMetadata( + variationObj.getString("id"), + variationObj.optString("name", ""), + variationObj.optBoolean("reference", false), + variationObj.optInt("allocation", if (bucketingMode) 0 else 100), + variationGroupMetadata + ) + val flags = + parse_flags(variationObj.getJSONObject("modifications"), variationMetadata) //In Api mode always 100%, in bucketing mode the variations at 0% are loaded just to check if it matches one in cache at selection time. - Variation(campaignId, variationGroupId, variationId, isReference, modifications, allocation) + Variation(flags, variationMetadata) } catch (e: Exception) { - FlagshipLogManager.log(FlagshipLogManager.Tag.PARSING, LogManager.Level.ERROR, - FlagshipConstants.Errors.PARSING_VARIATION_ERROR) + FlagshipLogManager.log( + FlagshipLogManager.Tag.PARSING, LogManager.Level.ERROR, + FlagshipConstants.Errors.PARSING_VARIATION_ERROR + ) null } } - } - fun getModificationsValues(): HashMap? { - return modifications?.values + fun parse_flags( + modificationsObj: JSONObject, + variationMetadata: VariationMetadata + ): HashMap? { + return try { + val flags: HashMap = HashMap() + val type = modificationsObj.getString("type") + 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) + flags[key] = _Flag(key, value, FlagMetadata(variationMetadata)) + else + FlagshipLogManager.log( + FlagshipLogManager.Tag.PARSING, + LogManager.Level.ERROR, + FlagshipConstants.Errors.PARSING_MODIFICATION_ERROR + ) + } + flags + } catch (e: Exception) { + FlagshipLogManager.log( + FlagshipLogManager.Tag.PARSING, + LogManager.Level.ERROR, + FlagshipConstants.Errors.PARSING_MODIFICATION_ERROR + ) + null + } + } } override fun toString(): String { - return "Variation(campaignId='$campaignId', variationGroupId='$variationGroupId', " + - "variationId='$variationId', isReference=$isReference, " + - "modifications=$modifications, allocation=$allocation)" + return "Variation(campaignId='${variationMetadata.campaignId}', variationGroupId='${variationMetadata.variationGroupId}', " + + "variationId='${variationMetadata.variationId}', isReference=${variationMetadata.isReference}, " + + "flags=$flags, allocation=${variationMetadata.allocation})" } } diff --git a/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt b/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt index 82bc22a..e634cea 100644 --- a/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt +++ b/flagship/src/main/java/com/abtasty/flagship/model/VariationGroup.kt @@ -7,30 +7,46 @@ import com.abtasty.flagship.utils.MurmurHash import com.abtasty.flagship.visitor.VisitorDelegateDTO import org.json.JSONObject -data class VariationGroup(val campaignId: String, val variationGroupId: String, - val variations: LinkedHashMap?, val targetingGroups: TargetingGroups?) { +data class VariationGroup( + val variationGroupMetadata: VariationGroupMetadata, + val variations: LinkedHashMap?, + val targetingGroups: TargetingGroups?, +) { fun selectVariation(visitorDelegateDTO: VisitorDelegateDTO): Variation? { variations?.let { variations -> - val cachedVariationId = visitorDelegateDTO.getVariationGroupAssignment(variationGroupId) - val cachedVariationEntry = variations.entries.firstOrNull { e -> e.value.variationId == cachedVariationId } + val cachedVariationId = + visitorDelegateDTO.getVariationGroupAssignment(variationGroupMetadata.variationGroupId) + val cachedVariationEntry = + variations.entries.firstOrNull { e -> e.value.variationMetadata.variationId == cachedVariationId } when { cachedVariationEntry != null -> { val variation = cachedVariationEntry.value - FlagshipLogManager.log(FlagshipLogManager.Tag.ALLOCATION, LogManager.Level.DEBUG, - FlagshipConstants.Info.CACHED_ALLOCATION.format(variation.variationId)) + FlagshipLogManager.log( + FlagshipLogManager.Tag.ALLOCATION, LogManager.Level.DEBUG, + FlagshipConstants.Info.CACHED_ALLOCATION.format(variation.variationMetadata.variationId) + ) return variation } + cachedVariationId != null -> return null else -> { var p = 0 - val murmurAllocation: Int = MurmurHash.getAllocationFromMurmur(variationGroupId, visitorDelegateDTO.visitorId) + val murmurAllocation: Int = MurmurHash.getAllocationFromMurmur( + variationGroupMetadata.variationGroupId, + visitorDelegateDTO.visitorId + ) for ((variationId, variation) in variations) { - if (variation.allocation > 0) { //Variation with 0% are only loaded to check if it matches one from the cache, and should be ignored otherwise. - p += variation.allocation + if (variation.variationMetadata.allocation > 0) { //Variation with 0% are only loaded to check if it matches one from the cache, and should be ignored otherwise. + p += variation.variationMetadata.allocation if (murmurAllocation < p) { - FlagshipLogManager.log(FlagshipLogManager.Tag.ALLOCATION, LogManager.Level.DEBUG, FlagshipConstants.Info.NEW_ALLOCATION.format( - variation.variationId, murmurAllocation)) + FlagshipLogManager.log( + FlagshipLogManager.Tag.ALLOCATION, + LogManager.Level.DEBUG, + FlagshipConstants.Info.NEW_ALLOCATION.format( + variation.variationMetadata.variationId, murmurAllocation + ) + ) return variation } } @@ -46,25 +62,40 @@ data class VariationGroup(val campaignId: String, val variationGroupId: String, } companion object { - fun parse(campaignId: String, campaignType: String, slug: String, variationGroupsObj: JSONObject, bucketing: Boolean): VariationGroup? { + fun parse( + variationGroupsObj: JSONObject, + bucketing: Boolean, + campaignMetadata: CampaignMetadata + ): VariationGroup? { return try { - val variationGroupId = - variationGroupsObj.getString(if (bucketing) "id" else "variationGroupId") + val variationGroupMetadata = VariationGroupMetadata( + variationGroupsObj.getString(if (bucketing) "id" else "variationGroupId"), + variationGroupsObj.optString(if (bucketing) "name" else "variationGroupName", ""), + campaignMetadata + ) var targetingGroups: TargetingGroups? = null val variations: LinkedHashMap = LinkedHashMap() if (!bucketing) { // api variationGroupsObj.optJSONObject("variation")?.let { variationObj -> - Variation.parse(bucketing, campaignId, campaignType, slug, variationGroupId, variationObj)?.let { variation -> - variations[variation.variationId] = variation + Variation.parse( + variationObj, + bucketing, + variationGroupMetadata, + )?.let { variation -> + variations[variation.variationMetadata.variationId] = variation } } } else { //bucketing variationGroupsObj.optJSONArray("variations")?.let { variationArr -> for (variationObj in variationArr) { - Variation.parse(bucketing, campaignId, campaignType, slug, variationGroupId, variationObj)?.let { variation -> - variations[variation.variationId] = variation + Variation.parse( + variationObj, + bucketing, + variationGroupMetadata + )?.let { variation -> + variations[variation.variationMetadata.variationId] = variation } } variationGroupsObj.optJSONObject("targeting")?.let { targetingObj -> @@ -74,8 +105,9 @@ data class VariationGroup(val campaignId: String, val variationGroupId: String, } } } - VariationGroup(campaignId, variationGroupId, variations, targetingGroups) + VariationGroup(variationGroupMetadata, variations, targetingGroups) } catch (e: Exception) { + e.printStackTrace() FlagshipLogManager.log( FlagshipLogManager.Tag.PARSING, LogManager.Level.ERROR, FlagshipConstants.Errors.PARSING_VARIATION_GROUP_ERROR @@ -86,6 +118,6 @@ data class VariationGroup(val campaignId: String, val variationGroupId: String, } override fun toString(): String { - return "VariationGroup(campaignId='$campaignId', variationGroupId='$variationGroupId', variations=$variations, targetingGroups=$targetingGroups)" + return "VariationGroup(campaignId='${variationGroupMetadata.campaignId}', variationGroupId='${variationGroupMetadata.variationGroupId}', variations=$variations, targetingGroups=$targetingGroups)" } } \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/model/VariationGroupMetadata.kt b/flagship/src/main/java/com/abtasty/flagship/model/VariationGroupMetadata.kt new file mode 100644 index 0000000..80af1dd --- /dev/null +++ b/flagship/src/main/java/com/abtasty/flagship/model/VariationGroupMetadata.kt @@ -0,0 +1,21 @@ +package com.abtasty.flagship.model + +open class VariationGroupMetadata( + /** + * Id of the variation group from which the Flags is from + */ + val variationGroupId: String = "", + + /** + * Id of the variation group from which the Flags is from + */ + val variationGroupName: String = "", + campaignMetadata: CampaignMetadata +) : CampaignMetadata(campaignMetadata) { + + constructor(variationGroupMetadata: VariationGroupMetadata) : this( + variationGroupMetadata.variationGroupId, + variationGroupMetadata.variationGroupName, + variationGroupMetadata + ) +} \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/model/VariationMetadata.kt b/flagship/src/main/java/com/abtasty/flagship/model/VariationMetadata.kt new file mode 100644 index 0000000..8c04bee --- /dev/null +++ b/flagship/src/main/java/com/abtasty/flagship/model/VariationMetadata.kt @@ -0,0 +1,35 @@ +package com.abtasty.flagship.model + +open class VariationMetadata( + /** + * Id of the variation from which the Flags is from. + */ + val variationId: String = "", + + /** + * Name of the variation from which the Flags is from. + */ + val variationName: String = "", + + /** + * Is the Flag from a reference variation. + */ + val isReference: Boolean = false, + + /** + * Variation allocation + */ + internal val allocation: Int = 100, + variationGroupMetadata: VariationGroupMetadata +) : VariationGroupMetadata( + variationGroupMetadata +) { + + constructor(variationMetadata: VariationMetadata) : this( + variationMetadata.variationId, + variationMetadata.variationName, + variationMetadata.isReference, + variationMetadata.allocation, + variationMetadata + ) +} \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/utils/EFlagFetchingStatus.kt b/flagship/src/main/java/com/abtasty/flagship/utils/EFlagFetchingStatus.kt new file mode 100644 index 0000000..7c32b8e --- /dev/null +++ b/flagship/src/main/java/com/abtasty/flagship/utils/EFlagFetchingStatus.kt @@ -0,0 +1,9 @@ +package com.abtasty.flagship.utils + +enum class EVisitorFlagsUpdateStatus { + CREATED, + CONTEXT_UPDATED, + FLAGS_FETCHED, + AUTHENTICATED, + UNAUTHENTICATED +} diff --git a/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipConstants.kt b/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipConstants.kt index 355748c..0a855fe 100644 --- a/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipConstants.kt +++ b/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipConstants.kt @@ -29,7 +29,7 @@ class FlagshipConstants { val FLAG_MISSING_ERROR = "Flag not found." val FLAG_VALUE_ERROR = "Default value will be returned for flag '%s': " - val FLAG_USER_EXPOSITION_ERROR = "User exposition for Flag '%s' wont be sent: " + val FLAG_USER_EXPOSITION_ERROR = "Visitor exposition for Flag '%s' wont be sent: " val FLAG_METADATA_ERROR = "Empty metadata will be returned for Flag '%s': " val HIT_INVALID_DATA_ERROR = "'%s' hit invalid format error. \n %s" @@ -75,6 +75,16 @@ class FlagshipConstants { "Panic mode is enabled : all features are disabled except 'fetchFlags()'." val CONTEXT_VALUE_OVERRIDING = "key '%s' is overriding a predefined flagship value" + + val FLAGS_CREATED = "Visitor '%s' has been created without calling `fetchFlags()` method afterwards, the value of the flag `flagKey` may be outdated." + + val FLAGS_CONTEXT_UPDATED = "Visitor context for visitor '%s' has been updated without calling `fetchFlags()` method afterwards, the value of the flag `flagKey` may be outdated." + + val FLAGS_AUTHENTICATED = "Visitor '%s' has been authenticated without calling `fetchFlags()` method afterwards, the value of the flag `flagKey` may be outdated." + + val FLAGS_UNAUTHENTICATED = "Visitor '%s' has been unauthenticated without calling `fetchFlags()` method afterwards, the value of the flag `flagKey` may be outdated." + + } } diff --git a/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipLogManager.kt b/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipLogManager.kt index 3f12c48..ff4d688 100644 --- a/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipLogManager.kt +++ b/flagship/src/main/java/com/abtasty/flagship/utils/FlagshipLogManager.kt @@ -27,7 +27,7 @@ class FlagshipLogManager(level: Level = Level.ALL) : LogManager(level) { FLAGS_FETCH("FLAGS_FETCH"), FLAG_VALUE("FLAG_VALUE"), FLAG_METADATA("FLAG_METADATA"), - FLAG_USER_EXPOSED("FLAG_USER_EXPOSED"), + FLAG_VISITOR_EXPOSED("FLAG_VISITOR_EXPOSED"), ACTIVATE("ACTIVATE"), TRACKING("HIT"), AUTHENTICATE("AUTHENTICATE"), 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 6189aea..b249dd7 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/DefaultStrategy.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/DefaultStrategy.kt @@ -1,7 +1,6 @@ package com.abtasty.flagship.visitor import com.abtasty.flagship.api.TrackingManager -import com.abtasty.flagship.cache.CacheHelper import com.abtasty.flagship.cache.HitCacheHelper import com.abtasty.flagship.cache.VisitorCacheHelper import com.abtasty.flagship.decision.DecisionManager @@ -9,8 +8,11 @@ import com.abtasty.flagship.hits.Activate import com.abtasty.flagship.hits.Consent import com.abtasty.flagship.hits.Hit import com.abtasty.flagship.main.Flagship +import com.abtasty.flagship.model.ExposedFlag import com.abtasty.flagship.model.Flag -import com.abtasty.flagship.model.Modification +import com.abtasty.flagship.model.FlagMetadata +import com.abtasty.flagship.model._Flag +import com.abtasty.flagship.utils.EVisitorFlagsUpdateStatus import com.abtasty.flagship.utils.FlagshipConstants import com.abtasty.flagship.utils.FlagshipContext import com.abtasty.flagship.utils.FlagshipLogManager @@ -31,6 +33,7 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) override fun updateContext(key: String, value: T) { + val copyBefore = HashMap(visitor.visitorContext) when (true) { (!(value is String || value is Number || value is Boolean)) -> FlagshipLogManager.log(FlagshipLogManager.Tag.UPDATE_CONTEXT, @@ -43,16 +46,24 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) String.format(FlagshipConstants.Errors.CONTEXT_RESERVED_KEY_ERROR, key)) else -> visitor.visitorContext[key] = value } + if (copyBefore != HashMap(visitor.visitorContext)) + visitor.flagFetchingStatus = EVisitorFlagsUpdateStatus.CONTEXT_UPDATED } override fun updateContext(flagshipContext: FlagshipContext, value: T) { + val copyBefore = HashMap(visitor.visitorContext) if (flagshipContext.verify(value)) visitor.visitorContext[flagshipContext.key] = value + if (copyBefore != HashMap(visitor.visitorContext)) + visitor.flagFetchingStatus = EVisitorFlagsUpdateStatus.CONTEXT_UPDATED } override fun clearContext() { + val copyBefore = HashMap(visitor.visitorContext) visitor.visitorContext.clear() visitor.loadContext(null) + if (copyBefore != HashMap(visitor.visitorContext)) + visitor.flagFetchingStatus = EVisitorFlagsUpdateStatus.CONTEXT_UPDATED } override fun fetchFlags(): Deferred { @@ -60,10 +71,11 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) return Flagship.coroutineScope().async { decisionManager?.let { val visitorDTO = visitor.toDTO() - decisionManager.getCampaignsModifications(visitorDTO)?.let { campaigns -> - visitor.updateModifications(campaigns) + decisionManager.getCampaignFlags(visitorDTO)?.let { flags -> + visitor.updateFlags(flags) visitor.logVisitor(FlagshipLogManager.Tag.FLAGS_FETCH) visitor.getStrategy().cacheVisitor() + visitor.flagFetchingStatus = EVisitorFlagsUpdateStatus.FLAGS_FETCHED } } } @@ -74,65 +86,90 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) trackingManager.sendContextRequest(visitor.toDTO()) } - @Throws( - FlagshipConstants.Exceptions.Companion.FlagTypeException::class, - FlagshipConstants.Exceptions.Companion.FlagException::class, - FlagshipConstants.Exceptions.Companion.FlagNotFoundException::class - ) - fun getModification(key: String, defaultValue: T?): Modification { - val visitorModifications = visitor.modifications.toMap() - try { - val modification = visitorModifications[key] - if (modification != null) { - val castValue = (modification.value ?: defaultValue) as T + fun getVisitorFlag(key: String, defaultValue: Any?) : _Flag { + visitor.flags[key]?.let { flag -> + try { + val castValue = (flag.value ?: defaultValue) if (defaultValue == null || castValue == null || castValue.javaClass == defaultValue.javaClass) - return modification + return flag else throw FlagshipConstants.Exceptions.Companion.FlagTypeException() - } else - throw FlagshipConstants.Exceptions.Companion.FlagNotFoundException() - } catch (e: Exception) { - when (e) { - is FlagshipConstants.Exceptions.Companion.FlagTypeException -> throw e - is FlagshipConstants.Exceptions.Companion.FlagNotFoundException -> throw e - else -> throw FlagshipConstants.Exceptions.Companion.FlagException() + } catch (e: Exception) { + throw e } } + throw FlagshipConstants.Exceptions.Companion.FlagNotFoundException() } @Suppress("unchecked_cast") - override fun getFlagValue(key: String, defaultValue: T?) : T? { + override fun getVisitorFlagValue(key: String, defaultValue: T?): T? { try { - val modification = getModification(key, defaultValue) - return (modification.value ?: defaultValue) as T - } catch (e : Exception) { - logFlagError(FlagshipLogManager.Tag.FLAG_VALUE, e, FlagshipConstants.Errors.FLAG_VALUE_ERROR.format(key)) + val flag = getVisitorFlag(key, defaultValue) + return (flag.value ?: defaultValue) as T + } catch (e: Exception) { + logFlagError( + FlagshipLogManager.Tag.FLAG_VALUE, + e, + FlagshipConstants.Errors.FLAG_VALUE_ERROR.format(key) + ) } return defaultValue } - override fun exposeFlag(key: String, defaultValue: T?) { + override fun getVisitorFlagMetadata(key: String, defaultValue: T?): FlagMetadata? { try { - val modification = getModification(key, defaultValue) - if (!visitor.activatedVariations.contains(modification.variationId)) - visitor.activatedVariations.add(modification.variationId) - sendHit(Activate(modification)) + return getVisitorFlag(key, defaultValue).metadata } catch (e: Exception) { - logFlagError(FlagshipLogManager.Tag.FLAG_USER_EXPOSED, e, FlagshipConstants.Errors.FLAG_USER_EXPOSITION_ERROR.format(key)) + logFlagError(FlagshipLogManager.Tag.FLAG_METADATA, e, FlagshipConstants.Errors.FLAG_METADATA_ERROR.format(key)) } + return null } - override fun getFlagMetadata(key: String, defaultValue: T?) : Modification? { + override fun sendVisitorExposition(key: String, defaultValue: T?) { try { - return getModification(key, defaultValue) + val flag = getVisitorFlag(key, defaultValue) + if (!visitor.activatedVariations.contains(flag.metadata.variationId)) + visitor.activatedVariations.add(flag.metadata.variationId) + val trackingManager: TrackingManager = configManager.trackingManager + val activationResult = + trackingManager.sendHit(visitor.toDTO(), Activate(flag.metadata)) + activationResult?.invokeOnCompletion { it -> + runBlocking { + if (it == null) { + try { + if (activationResult.await()) + Flagship.getConfig().onVisitorExposed?.invoke( + VisitorExposed( + visitor.visitorId, + visitor.anonymousId, + HashMap(visitor.visitorContext), + visitor.isAuthenticated, + visitor.hasConsented + ), + ExposedFlag(flag.key, flag.value, defaultValue, flag.metadata) + ) + } catch (e: Exception) { + FlagshipLogManager.log( + FlagshipLogManager.Tag.TRACKING, + LogManager.Level.ERROR, + e.stackTraceToString() + ) + } + } + } + } } catch (e: Exception) { - logFlagError(FlagshipLogManager.Tag.FLAG_METADATA, e, FlagshipConstants.Errors.FLAG_METADATA_ERROR.format(key)) + logFlagError( + FlagshipLogManager.Tag.FLAG_VISITOR_EXPOSED, + e, + FlagshipConstants.Errors.FLAG_USER_EXPOSITION_ERROR.format(key) + ) } - return null } - override fun getFlag(key: String, defaultValue : T): Flag { + override fun getFlag(key: String, defaultValue: T): Flag { + checkOutDatedFlags() return Flag(visitor, key, defaultValue) } @@ -148,10 +185,13 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) override fun authenticate(visitorId: String) { if (visitor.configManager.isDecisionMode(Flagship.DecisionMode.DECISION_API)) { + val changed = visitor.visitorId != visitorId if (visitor.anonymousId == null) visitor.anonymousId = visitor.visitorId visitor.visitorId = visitorId visitor.isAuthenticated = true // todo CHECK IF OK + if (changed) + visitor.flagFetchingStatus = EVisitorFlagsUpdateStatus.AUTHENTICATED } else { FlagshipLogManager.log(FlagshipLogManager.Tag.AUTHENTICATE, LogManager.Level.ERROR, String.format(FlagshipConstants.Errors.AUTHENTICATION_BUCKETING_ERROR, "authenticate")) @@ -164,6 +204,7 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) visitor.visitorId = visitor.anonymousId ?: "" //todo is needed to generate visitor.anonymousId = null visitor.isAuthenticated = false // todo CHECK IF OK + visitor.flagFetchingStatus = EVisitorFlagsUpdateStatus.UNAUTHENTICATED } } else { FlagshipLogManager.log(FlagshipLogManager.Tag.UNAUTHENTICATE, LogManager.Level.ERROR, @@ -230,7 +271,12 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) cancel() } } - val isSuccess = lookupLatch.await(flagshipConfig.cacheManager.visitorCacheLookupTimeout, flagshipConfig.cacheManager.timeoutUnit) + val isSuccess = withContext(Dispatchers.IO) { + lookupLatch.await( + flagshipConfig.cacheManager.visitorCacheLookupTimeout, + flagshipConfig.cacheManager.timeoutUnit + ) + } if (!isSuccess) { coroutine.cancelAndJoin() logCacheError(FlagshipConstants.Errors.CACHE_IMPL_TIMEOUT.format("lookupVisitor", visitorDelegateDTO.visitorId)) @@ -292,4 +338,18 @@ open class DefaultStrategy(visitor: VisitorDelegate) : VisitorStrategy(visitor) } } } + + private fun checkOutDatedFlags() { + if (visitor.flagFetchingStatus != EVisitorFlagsUpdateStatus.FLAGS_FETCHED) { + val warningString : String = when (visitor.flagFetchingStatus) { + EVisitorFlagsUpdateStatus.CONTEXT_UPDATED -> FlagshipConstants.Warnings.FLAGS_CONTEXT_UPDATED + EVisitorFlagsUpdateStatus.AUTHENTICATED -> FlagshipConstants.Warnings.FLAGS_AUTHENTICATED + EVisitorFlagsUpdateStatus.UNAUTHENTICATED -> FlagshipConstants.Warnings.FLAGS_UNAUTHENTICATED + else -> { + FlagshipConstants.Warnings.FLAGS_CREATED + } + } + FlagshipLogManager.log(FlagshipLogManager.Tag.FLAGS_FETCH, LogManager.Level.WARNING, warningString.format(visitor.visitorId)) + } + } } \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/visitor/IVisitor.kt b/flagship/src/main/java/com/abtasty/flagship/visitor/IVisitor.kt index 514be39..026c383 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/IVisitor.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/IVisitor.kt @@ -65,7 +65,7 @@ interface IVisitor { * This function will return a flag object containing the current value returned by Flagship and the associated campaign information. * If the key is not found an empty Flag object with the default value will be returned. * - * @param key key associated to the modification. + * @param key key associated to the flag. * @param defaultValue fallback default value to use. */ fun getFlag(key: String, defaultValue : T) : Flag 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 f2bfe1c..a8429a3 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/NoConsentStrategy.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/NoConsentStrategy.kt @@ -1,6 +1,8 @@ package com.abtasty.flagship.visitor import com.abtasty.flagship.hits.Hit +import com.abtasty.flagship.model.FlagMetadata +import com.abtasty.flagship.model._Flag import com.abtasty.flagship.utils.FlagshipConstants import com.abtasty.flagship.utils.FlagshipLogManager import com.abtasty.flagship.utils.LogManager @@ -21,8 +23,8 @@ class NoConsentStrategy(val visitorDelegate: VisitorDelegate) : DefaultStrategy( FlagshipLogManager.log(tag!!, LogManager.Level.ERROR, String.format(FlagshipConstants.Errors.METHOD_DEACTIVATED_CONSENT_ERROR, methodName, visitorId)) } - override fun exposeFlag(key: String, defaultValue : T?) { - logMethodDeactivatedError(FlagshipLogManager.Tag.FLAG_USER_EXPOSED, visitorDelegate.visitorId, "Flag.userExposed()") + override fun sendVisitorExposition(key: String, defaultValue : T?) { + logMethodDeactivatedError(FlagshipLogManager.Tag.FLAG_VISITOR_EXPOSED, visitorDelegate.visitorId, "Flag[$key].visitorExposed()") } override fun sendHit(hit: Hit) { 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 5a96a0f..ae4f6b1 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/NotReadyStrategy.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/NotReadyStrategy.kt @@ -2,7 +2,9 @@ package com.abtasty.flagship.visitor 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.FlagshipLogManager import kotlinx.coroutines.Deferred import kotlinx.coroutines.async @@ -15,19 +17,18 @@ class NotReadyStrategy(val visitorDelegate: VisitorDelegate) : DefaultStrategy(v return Flagship.coroutineScope().async { } //do nothing } - override fun getFlagValue(key: String, defaultValue: T?): T? { - logMethodDeactivatedError(FlagshipLogManager.Tag.FLAG_VALUE, "Flag.value()") + override fun getVisitorFlagValue(key: String, defaultValue: T?): T? { + logMethodDeactivatedError(FlagshipLogManager.Tag.FLAG_VALUE, "Flag[$key].value()") return defaultValue } - - override fun getFlagMetadata(key: String, defaultValue: T?): Modification? { - logMethodDeactivatedError(FlagshipLogManager.Tag.FLAG_METADATA, "Flag.metadata()") + override fun getVisitorFlagMetadata(key: String, defaultValue: T?): FlagMetadata? { + logMethodDeactivatedError(FlagshipLogManager.Tag.FLAG_METADATA, "Flag[$key].metadata()") return null } - override fun exposeFlag(key: String, defaultValue: T?) { - logMethodDeactivatedError(FlagshipLogManager.Tag.FLAG_USER_EXPOSED, "Flag.userExposed()") + override fun sendVisitorExposition(key: String, defaultValue: T?) { + logMethodDeactivatedError(FlagshipLogManager.Tag.FLAG_VISITOR_EXPOSED, "Flag[$key].visitorExposed()") } override fun sendHit(hit: Hit) { 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 fd9dad7..017b5cf 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/PanicStrategy.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/PanicStrategy.kt @@ -1,7 +1,9 @@ package com.abtasty.flagship.visitor import com.abtasty.flagship.hits.Hit +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 org.json.JSONObject @@ -27,18 +29,18 @@ class PanicStrategy(val visitorDelegate: VisitorDelegate) : DefaultStrategy(visi // Call default strategy fetchFlags - override fun getFlagValue(key: String, defaultValue: T?): T? { - logMethodDeactivatedError(FlagshipLogManager.Tag.FLAG_VALUE, "Flag.value()") + override fun getVisitorFlagValue(key: String, defaultValue: T?): T? { + logMethodDeactivatedError(FlagshipLogManager.Tag.FLAG_VALUE, "Flag[$key].value()") return defaultValue } - override fun getFlagMetadata(key: String, defaultValue: T?): Modification? { - logMethodDeactivatedError(FlagshipLogManager.Tag.FLAG_METADATA, "Flag.metadata()") + override fun getVisitorFlagMetadata(key: String, defaultValue: T?): FlagMetadata? { + logMethodDeactivatedError(FlagshipLogManager.Tag.FLAG_METADATA, "Flag[$key].metadata()") return null } - override fun exposeFlag(key: String, defaultValue: T?) { - logMethodDeactivatedError(FlagshipLogManager.Tag.FLAG_USER_EXPOSED, "Flag.userExposed()") + override fun sendVisitorExposition(key: String, defaultValue: T?) { + logMethodDeactivatedError(FlagshipLogManager.Tag.FLAG_VISITOR_EXPOSED, "Flag[$key].visitorExposed()") } override fun sendHit(hit: Hit) { 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 f926aa3..3428f6a 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/Visitor.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/Visitor.kt @@ -7,7 +7,6 @@ import com.abtasty.flagship.model.Flag import com.abtasty.flagship.utils.FlagshipContext import kotlinx.coroutines.Deferred import org.json.JSONObject -import java.time.Instant import java.util.* /** @@ -153,8 +152,8 @@ class Visitor(internal val configManager: ConfigManager, visitorId: String, isAu @Synchronized @Deprecated("Use getFlag(\"flagkey\").metadata instead.", ReplaceWith("getFlag(s).metadata"), DeprecationLevel.WARNING) fun getModificationInfo(key: String): JSONObject? { - val json = delegate.getStrategy().getFlag(key, null).metadata().toJson() - return if (json.length() == 0) null else json + val metadata = delegate.getStrategy().getFlag(key, null).metadata() + return if (metadata.exists()) metadata.toJson() else null } /** @@ -163,7 +162,7 @@ class Visitor(internal val configManager: ConfigManager, visitorId: String, isAu * @param key key which identify the modification to activate. */ @Synchronized - @Deprecated("Use getFlag(\"flagkey\").userExposed() instead.", ReplaceWith("getFlag(s).useExposed()"), DeprecationLevel.WARNING) + @Deprecated("Use getFlag(\"flagkey\").visitorExposed() instead.", ReplaceWith("getFlag(s).visitorExposed()"), DeprecationLevel.WARNING) fun activateModification(key: String) { delegate.getStrategy().getFlag(key, null).userExposed() } 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 298c6b1..3bd0aca 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegate.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegate.kt @@ -2,15 +2,15 @@ package com.abtasty.flagship.visitor import com.abtasty.flagship.main.ConfigManager import com.abtasty.flagship.main.Flagship -import com.abtasty.flagship.model.Modification +import com.abtasty.flagship.model._Flag +import com.abtasty.flagship.utils.EVisitorFlagsUpdateStatus import com.abtasty.flagship.utils.FlagshipConstants import com.abtasty.flagship.utils.FlagshipLogManager import com.abtasty.flagship.utils.LogManager -import java.util.* +import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentMap -import kotlin.collections.HashMap /** * Delegate for Visitor @@ -21,11 +21,12 @@ class VisitorDelegate(internal val configManager: ConfigManager, visitorId: Stri var visitorId: String var anonymousId: String? = null var visitorContext: ConcurrentMap = ConcurrentHashMap() - var modifications: ConcurrentMap = ConcurrentHashMap() + var flags: ConcurrentMap = ConcurrentHashMap() var activatedVariations = ConcurrentLinkedQueue() var hasConsented: Boolean var isAuthenticated: Boolean var assignmentsHistory: ConcurrentMap = ConcurrentHashMap() + var flagFetchingStatus: EVisitorFlagsUpdateStatus? = null init { this.visitorId = if (visitorId == null || visitorId.isEmpty()) generateUUID() else visitorId @@ -38,6 +39,7 @@ class VisitorDelegate(internal val configManager: ConfigManager, visitorId: Stri // if (!this.hasConsented) getStrategy().sendConsentRequest() //Send anyway logVisitor(FlagshipLogManager.Tag.VISITOR) + flagFetchingStatus = EVisitorFlagsUpdateStatus.CREATED } internal fun getStrategy(): VisitorStrategy { @@ -80,11 +82,9 @@ class VisitorDelegate(internal val configManager: ConfigManager, visitorId: Stri return hasConsented } - internal fun updateModifications(modifications: HashMap?) { - if (modifications != null) { - this.modifications.clear() - this.modifications.putAll(modifications) - } + internal fun updateFlags(flags: HashMap) { + this.flags.clear() + this.flags.putAll(flags) } override fun toString(): String { diff --git a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegateDTO.kt b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegateDTO.kt index bfdaf5f..5835790 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegateDTO.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorDelegateDTO.kt @@ -10,7 +10,7 @@ open class VisitorDelegateDTO(val visitorDelegate: VisitorDelegate) { var visitorId = visitorDelegate.visitorId var anonymousId = visitorDelegate.anonymousId var context = HashMap(visitorDelegate.getContext()) - var modifications = HashMap(visitorDelegate.modifications) + var flags = HashMap(visitorDelegate.flags) var activatedVariations = ConcurrentLinkedQueue(visitorDelegate.activatedVariations) var hasConsented = visitorDelegate.hasConsented var isAuthenticated = visitorDelegate.isAuthenticated @@ -25,7 +25,7 @@ open class VisitorDelegateDTO(val visitorDelegate: VisitorDelegate) { json.put("isAuthenticated", isAuthenticated) json.put("hasConsented", hasConsented) json.put("context", contextToJson()) - json.put("modifications", modificationsToJson()) + json.put("flags", flagsToJson()) // json.put("activatedVariations", activatedVariationToJsonArray(activatedVariations)) json.put("activatedVariations", JSONArray(activatedVariations)) json.put("assignmentsHistory", JSONObject(assignmentsHistory as Map)) @@ -40,13 +40,13 @@ open class VisitorDelegateDTO(val visitorDelegate: VisitorDelegate) { return contextJson } - private fun modificationsToJson(): JSONObject { - val modificationJson = JSONObject() - for ((flag, modification) in this.modifications) { - val value: Any? = modification.value - modificationJson.put(flag, value ?: JSONObject.NULL) + private fun flagsToJson(): JSONObject { + val flagJson = JSONObject() + for ((k, flag) in this.flags) { + val value: Any? = flag.value + flagJson.put(k, value ?: JSONObject.NULL) } - return modificationJson + return flagJson } fun getVariationGroupAssignment(variationGroupId: String): String? { diff --git a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorExposed.kt b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorExposed.kt new file mode 100644 index 0000000..4add69c --- /dev/null +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorExposed.kt @@ -0,0 +1,11 @@ +package com.abtasty.flagship.visitor + +import java.util.HashMap + +data class VisitorExposed( + val visitorId: String, + val anonymousId: String?, + val context: HashMap, + val isAuthenticated: Boolean, + val hasConsented: Boolean +) \ No newline at end of file diff --git a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorStrategy.kt b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorStrategy.kt index 0ab4be5..05ee873 100644 --- a/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorStrategy.kt +++ b/flagship/src/main/java/com/abtasty/flagship/visitor/VisitorStrategy.kt @@ -1,7 +1,9 @@ package com.abtasty.flagship.visitor import com.abtasty.flagship.main.Flagship.getStatus +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 @@ -56,9 +58,7 @@ abstract class VisitorStrategy(var visitor: VisitorDelegate) : IVisitor { abstract fun flushHitCache() - abstract fun getFlagMetadata(key : String, defaultValue: T?) : Modification? - - abstract fun getFlagValue(key: String, defaultValue: T?) : T? - - abstract fun exposeFlag(key : String, defaultValue: T?) + abstract fun getVisitorFlagValue(key: String, defaultValue: T?): T? + abstract fun getVisitorFlagMetadata(key: String, defaultValue: T?): FlagMetadata? + abstract fun sendVisitorExposition(key: String, defaultValue: T?) } diff --git a/flagship/src/test/assets/api_response_1.json b/flagship/src/test/assets/api_response_1.json index 9274ac5..979befd 100644 --- a/flagship/src/test/assets/api_response_1.json +++ b/flagship/src/test/assets/api_response_1.json @@ -45,9 +45,14 @@ }, { "id": "c04bed3m649g0h999999", + "name": "my_release_campaign", + "type": "ab", + "slug": "my_release_slug", "variationGroupId": "c04bed3m649g0hAAAAAA", + "variationGroupName": "my_release_variation_group_name", "variation": { "id": "c04bed3m649g0hBBBBBB", + "name": "my_release_variation_name", "modifications": { "type": "FLAG", "value": { diff --git a/flagship/src/test/assets/bucketing_response_1.json b/flagship/src/test/assets/bucketing_response_1.json index 526d9aa..d4990aa 100644 --- a/flagship/src/test/assets/bucketing_response_1.json +++ b/flagship/src/test/assets/bucketing_response_1.json @@ -234,10 +234,12 @@ { "id": "brjjpk7734cg0sl5llll", "type": "ab", + "name": "my_campaign_name", "slug": "campaignSlug", "variationGroups": [ { "id": "brjjpk7734cg0sl5mmmm", + "name": "my_variation_group_name", "targeting": { "targetingGroups": [ { @@ -254,6 +256,7 @@ "variations": [ { "id": "brjjpk7734cg0sl5nnnn", + "name": "my_variation_name_0", "modifications": { "type": "JSON", "value": { @@ -266,6 +269,7 @@ }, { "id": "brjjpk7734cg0sl5oooo", + "name": "my_variation_name_1", "modifications": { "type": "JSON", "value": { @@ -277,6 +281,7 @@ }, { "id": "brjjpk7734cg0sl5pppp", + "name": "my_variation_name_2", "modifications": { "type": "JSON", "value": { @@ -288,6 +293,7 @@ }, { "id": "brjjpk7734cg0sl5qqqq", + "name": "my_variation_name_3", "modifications": { "type": "JSON", "value": { diff --git a/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt b/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt index 0284449..565da5f 100644 --- a/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt +++ b/flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt @@ -14,11 +14,15 @@ import com.abtasty.flagship.main.Flagship import com.abtasty.flagship.main.Flagship.start import com.abtasty.flagship.main.FlagshipConfig import com.abtasty.flagship.main.FlagshipConfig.Bucketing +import com.abtasty.flagship.model.ExposedFlag +import com.abtasty.flagship.model.Flag import com.abtasty.flagship.utils.ETargetingComp +import com.abtasty.flagship.utils.FlagshipConstants import com.abtasty.flagship.utils.FlagshipContext import com.abtasty.flagship.utils.FlagshipLogManager import com.abtasty.flagship.utils.LogManager import com.abtasty.flagship.visitor.Visitor +import com.abtasty.flagship.visitor.VisitorExposed import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import okhttp3.OkHttpClient @@ -47,7 +51,6 @@ class FlagshipTests { private val BUCKETING_URL = "https://cdn.flagship.io/%s/bucketing.json" private val ACTIVATION_URL = "https://decision.flagship.io/v2/activate" private val CONTEXT_URL = "https://decision.flagship.io/v2/%s/events" - private val CONSENT_PARAM = "&sendContextEvent=false" private val ARIANE_URL = "https://ariane.abtasty.com/" private val _ENV_ID_ = "_ENV_ID_" private val _API_KEY_ = "_API_KEY_" @@ -70,6 +73,7 @@ class FlagshipTests { FlagshipTestsHelper.interceptor().clearRules() Flagship.reset() Thread.sleep(50) + System.out.println("__TEAR DOWN__") } private fun overrideClient() { @@ -357,11 +361,26 @@ class FlagshipTests { assert(visitor.getModification("wrong", "value") == "value") assert(visitor.getModification("isref", 8) == 8) - assert(visitor.delegate.modifications.size == 8) + assert(visitor.delegate.flags.size == 8) assert(visitor.getModification("wrong_json", JSONObject()).toString() == JSONObject().toString()) Assert.assertNull(visitor.getModification("null", null)) assert(visitor.getModification("featureEnabled", true) == false) + val release = visitor.getFlag("release", -1) + assertEquals(100, release.value( false)) + assertTrue(release.exists()) + assertEquals("c04bed3m649g0h999999", release.metadata().campaignId) + assertEquals("c04bed3m649g0hAAAAAA", release.metadata().variationGroupId) + assertEquals("c04bed3m649g0hBBBBBB", release.metadata().variationId) + assertEquals("my_release_campaign", release.metadata().campaignName) + assertEquals("my_release_variation_group_name", release.metadata().variationGroupName) + assertEquals("my_release_variation_name", release.metadata().variationName) + assertEquals(false, release.metadata().isReference) + assertEquals("ab", release.metadata().campaignType) + assertEquals("my_release_slug", release.metadata().slug) + assertEquals(true, release.metadata().exists()) + assertEquals(9, release.metadata().toJson().length()) + //activations var activationLatch = CountDownLatch(1) val rule = FlagshipTestsHelper.interceptor().addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(ACTIVATION_URL) @@ -480,7 +499,7 @@ class FlagshipTests { runBlocking { visitor.synchronizeModifications().join() } - assert(visitor.delegate.modifications.size == 1) + assert(visitor.delegate.flags.size == 1) assert(visitor.getModification("featureEnabled", true) == false) visitor.updateContext( hashMapOf( @@ -549,7 +568,7 @@ class FlagshipTests { override fun onLog(level: Level, tag: String, message: String) { System.out.println(" ===> $tag $message") when (true) { - ((tag == "FLAG_USER_EXPOSED") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "FLAG_VISITOR_EXPOSED") && (message.contains("deactivated"))) -> logLatch.countDown() ((tag == "TRACKING") && (message.contains("deactivated"))) -> logLatch.countDown() ((tag == "FLAG_VALUE") && (message.contains("deactivated"))) -> logLatch.countDown() ((tag == "FLAG_METADATA") && (message.contains("deactivated"))) -> logLatch.countDown() @@ -604,7 +623,7 @@ class FlagshipTests { override fun onLog(level: Level, tag: String, message: String) { System.out.println(" ===> $tag $message") when (true) { - ((tag == "FLAG_USER_EXPOSED") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "FLAG_VISITOR_EXPOSED") && (message.contains("deactivated"))) -> logLatch.countDown() ((tag == "TRACKING") && (message.contains("deactivated"))) -> logLatch.countDown() ((tag == "FLAG_VALUE") && (message.contains("deactivated"))) -> logLatch.countDown() ((tag == "FLAG_METADATA") && (message.contains("deactivated"))) -> logLatch.countDown() @@ -636,19 +655,24 @@ class FlagshipTests { @Test fun test_visitor_strategy_no_consent() { - var urlConsent = 0 - var urlNoConsent = 0 + var consent = 0 + var noConsent = 0 val consentLatch = CountDownLatch(5) val logLatch = CountDownLatch(10) FlagshipTestsHelper.interceptor().addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(CAMPAIGNS_URL.format(_ENV_ID_)) - .returnResponse { request, i -> - urlConsent += 1 - FlagshipTestsHelper.responseFromAssets(ApplicationProvider.getApplicationContext(), "api_response_1.json", 200) + .verifyRequest { request, nbCall -> + val json = HttpCompat.requestJson(request) + try { + if (!json.getBoolean("visitor_consent")) + noConsent += 1 + if (json.getBoolean("visitor_consent")) + consent += 1 + } catch (e : Exception) { + + } + } - .build()) - FlagshipTestsHelper.interceptor().addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(CAMPAIGNS_URL.format(_ENV_ID_)+CONSENT_PARAM) .returnResponse { request, i -> - urlNoConsent += 1 FlagshipTestsHelper.responseFromAssets(ApplicationProvider.getApplicationContext(), "api_response_1.json", 200) } .build()) @@ -667,13 +691,7 @@ class FlagshipTests { assert(json.getString("ea") == "fs_consent") assert(json.getString("el").contains("android:")) assert(json.getString("vid") == "visitor") -// when (i) { -// 1 -> assert(json.getString("el").contains("android:false")) -// 2 -> assert(json.getString("el").contains("android:true")) -// 3 -> assert(json.getString("el").contains("android:false")) -// } val label = json.getString("el") -// assert(label == "android:false" || label == "android:true") if (label == "android:false") consentFalseLatch.countDown() if (label == "android:true") @@ -686,7 +704,7 @@ class FlagshipTests { override fun onLog(level: Level, tag: String, message: String) { System.out.println(" ===> $tag $message") when (true) { - ((tag == "FLAG_USER_EXPOSED") && (message.contains("deactivated"))) -> logLatch.countDown() + ((tag == "FLAG_VISITOR_EXPOSED") && (message.contains("deactivated"))) -> logLatch.countDown() ((tag == "TRACKING") && (message.contains("deactivated"))) -> logLatch.countDown() ((tag == "FLAG_VALUE") && (message.contains("deactivated"))) -> logLatch.countDown() ((tag == "FLAG_METADATA") && (message.contains("deactivated"))) -> logLatch.countDown() @@ -719,8 +737,8 @@ class FlagshipTests { runBlocking { visitor.synchronizeModifications().join() } - assert(urlConsent == 1) - assert(urlNoConsent == 2) + assert(consent == 1) + assert(noConsent == 2) consentLatch.await(500, TimeUnit.MILLISECONDS) if (!consentTrueLatch.await(500, TimeUnit.MILLISECONDS)) fail() @@ -1435,20 +1453,27 @@ class FlagshipTests { @Test fun flags() { +// Flagship.reset() +// Thread.sleep(500) val activateLatch = CountDownLatch(10) - FlagshipTestsHelper.interceptor() - .addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(ARIANE_URL) - .returnResponse { request, i -> - FlagshipTestsHelper.response("", 200) - } - .build()) + FlagshipTestsHelper.interceptor() + .addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(ARIANE_URL) + .returnResponse { request, i -> + FlagshipTestsHelper.response("", 200) + } + .build()) - FlagshipTestsHelper.interceptor().addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(BUCKETING_URL.format(_ENV_ID_)) - .returnResponse { request, i -> - FlagshipTestsHelper.responseFromAssets(ApplicationProvider.getApplicationContext(), "bucketing_response_1.json", 200) - } - .build()) + FlagshipTestsHelper.interceptor() + .addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(BUCKETING_URL.format(_ENV_ID_)) + .returnResponse { request, i -> + FlagshipTestsHelper.responseFromAssets( + ApplicationProvider.getApplicationContext(), + "bucketing_response_1.json", + 200 + ) + } + .build()) val readyLatch = CountDownLatch(1) @@ -1474,27 +1499,31 @@ class FlagshipTests { runBlocking { visitor.fetchFlags().await() } - assertEquals(81111, rank.value(true)) // activate val rank_plus = visitor.getFlag("rank_plus", "a") val rank_plus2 = visitor.getFlag("rank_plus", null) - assertEquals("a", rank_plus.value(false)) - assertNull(rank_plus2.value(false)) + assertEquals("a", rank_plus.value(false)) // no activate + assertNull(rank_plus2.value(false)) // no activate assertTrue(rank_plus.exists()) assertEquals("brjjpk7734cg0sl5llll", rank_plus.metadata().campaignId) assertEquals("brjjpk7734cg0sl5mmmm", rank_plus.metadata().variationGroupId) assertEquals("brjjpk7734cg0sl5oooo", rank_plus.metadata().variationId) + assertEquals("my_campaign_name", rank_plus.metadata().campaignName) + assertEquals("my_variation_group_name", rank_plus.metadata().variationGroupName) + assertEquals("my_variation_name_1", rank_plus.metadata().variationName) assertEquals(false, rank_plus.metadata().isReference) assertEquals("ab", rank_plus.metadata().campaignType) assertEquals(true, rank_plus.metadata().exists()) - assertEquals(6, rank_plus.metadata().toJson().length()) - + assertEquals(9, rank_plus.metadata().toJson().length()) val do_not_exists = visitor.getFlag("do_not_exists", "a") - assertEquals("a", do_not_exists.value( false)) + assertEquals("a", do_not_exists.value( false)) // no activate assertFalse(do_not_exists.exists()) assertEquals("", do_not_exists.metadata().campaignId) assertEquals("", do_not_exists.metadata().variationGroupId) assertEquals("", do_not_exists.metadata().variationId) + assertEquals("", do_not_exists.metadata().campaignName) + assertEquals("", do_not_exists.metadata().variationGroupName) + assertEquals("", do_not_exists.metadata().variationName) assertEquals(false, do_not_exists.metadata().isReference) assertEquals("", do_not_exists.metadata().campaignType) assertEquals(false, do_not_exists.metadata().exists()) @@ -1502,15 +1531,13 @@ class FlagshipTests { assertEquals("a", visitor.getFlag("rank", "a").value(true)) // no activate assertEquals(81111, visitor.getFlag("rank", null).value(true)) // activate - assertNull(visitor.getFlag("null", null).value(true)) // no activate - visitor.getFlag("rank", null).userExposed() // activate visitor.getFlag("rank", "null").userExposed() // no activate visitor.getFlag("rank_plus", "null").userExposed() // activate visitor.getFlag("do_not_exists", "null").userExposed() // no activate - Thread.sleep(1500) + Thread.sleep(500) assertEquals(6, activateLatch.count) //testing slug @@ -1519,8 +1546,6 @@ class FlagshipTests { assertEquals("campaignSlug", visitor.getFlag("rank_plus", "#00000000").metadata().slug) assertEquals("", visitor.getFlag("eflzjefl", "#00000000").metadata().slug) -// System.out.println("=> " + visitor.getFlag("rank_plus", null).value(true)) - } @Test @@ -1606,4 +1631,187 @@ class FlagshipTests { assertEquals(date2, lastModified) assertEquals(4, JSONObject(content).getJSONArray("campaigns").length()) } + + @Test + public fun visitor_exposed() { + + FlagshipTestsHelper.interceptor() + .addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(ARIANE_URL) + .returnResponse { request, i -> FlagshipTestsHelper.response("", 500) } + .build()) + + FlagshipTestsHelper.interceptor() + .addRule( + FlagshipTestsHelper.HttpInterceptor.Rule.Builder(ACTIVATION_URL.format(_ENV_ID_)) + .returnResponse { request, i -> + FlagshipTestsHelper.response("", 200) + } + .build() + ) + + FlagshipTestsHelper.interceptor() + .addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(CONTEXT_URL.format(_ENV_ID_)) + .returnResponse { request, i -> + FlagshipTestsHelper.response("", 500) + } + .build()) + FlagshipTestsHelper.interceptor() + .addRule( + FlagshipTestsHelper.HttpInterceptor.Rule.Builder(CAMPAIGNS_URL.format(_ENV_ID_)) + .returnResponse( + FlagshipTestsHelper.responseFromAssets( + ApplicationProvider.getApplicationContext(), + "api_response_1.json", + 200 + ) + ) + .build() + ) + + val exposedLatch = CountDownLatch(10) + Flagship.start( + getApplication(), + _ENV_ID_, + _API_KEY_, + FlagshipConfig.DecisionApi().withOnVisitorExposed { visitorExposed : VisitorExposed, exposedFlag: ExposedFlag<*> -> + System.out.println("#VE ==> ${exposedLatch.count.toInt() }") + if (exposedLatch.count.toInt() == 10) { + assertEquals(visitorExposed.visitorId, "visitor_1234") + assertNull(visitorExposed.anonymousId) + assertEquals(visitorExposed.hasConsented, true) + assertTrue(visitorExposed.context.containsKey("plan") && visitorExposed.context["plan"] == "vip") + assertEquals(exposedFlag.key, "featureEnabled") + assertEquals(exposedFlag.defaultValue, true) + assertEquals(exposedFlag.value, false) + assertEquals(exposedFlag.metadata.exists(), true) + assertEquals(exposedFlag.metadata.campaignId, "bmsorfe4jaeg0g000000") + assertEquals(exposedFlag.metadata.variationGroupId, "bmsorfe4jaeg0g1111111") + assertEquals(exposedFlag.metadata.variationId, "bmsorfe4jaeg0g222222") + exposedLatch.countDown() + } else if (exposedLatch.count.toInt() == 9) { + assertEquals(visitorExposed.visitorId, "visitor_5678") + assertNull(visitorExposed.anonymousId) + assertEquals(visitorExposed.hasConsented, true) + assertTrue(visitorExposed.context.containsKey("plan") && visitorExposed.context["plan"] == "business") + assertEquals(exposedFlag.key, "ab10_variation") + assertEquals(exposedFlag.defaultValue, 0) + assertEquals(exposedFlag.value, 7) + assertEquals(exposedFlag.metadata.exists(), true) + assertEquals(exposedFlag.metadata.campaignId, "c27tejc3fk9jdbFFFFFF") + assertEquals(exposedFlag.metadata.variationGroupId, "c27tejc3fk9jdbGGGGGG") + assertEquals(exposedFlag.metadata.variationId, "c27tfn8bcahim7HHHHHH") + exposedLatch.countDown() + } else { + exposedLatch.countDown() + } + }) + Thread.sleep(100) + val visitor_1234 = + Flagship.newVisitor("visitor_1234").context(hashMapOf("plan" to "vip")).build() + val visitor_5678 = + Flagship.newVisitor("visitor_5678").context(hashMapOf("plan" to "business")).build() + runBlocking { + visitor_5678.fetchFlags().await() + visitor_1234.fetchFlags().await() + } + assertFalse(visitor_1234.getFlag("featureEnabled", true).value()!!) + Thread.sleep(200) + assertEquals(visitor_5678.getFlag("ab10_variation", 0).value()!!, 7) + Thread.sleep(200) + assertEquals(visitor_5678.getFlag("isref", 0).value()!!, 0) // Wrong type no activation + Thread.sleep(200) + assertEquals(8, exposedLatch.count.toInt()) + + FlagshipTestsHelper.interceptor().clearRules() + FlagshipTestsHelper.interceptor() + .addRule( + FlagshipTestsHelper.HttpInterceptor.Rule.Builder(ACTIVATION_URL.format(_ENV_ID_)) + .returnResponse { request, i -> + FlagshipTestsHelper.response("", 500) + } + .build() + ) + assertEquals(visitor_1234.getFlag("target", "string").value()!!, "is") + Thread.sleep(200) + assertEquals(8, exposedLatch.count.toInt()) +// Thread.sleep(20000) + } + @Test + fun testFlagsOutDatedWarning() { + FlagshipTestsHelper.interceptor() + .addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(ARIANE_URL) + .returnResponse { request, i -> FlagshipTestsHelper.response("", 200) } + .build()) + + FlagshipTestsHelper.interceptor() + .addRule( + FlagshipTestsHelper.HttpInterceptor.Rule.Builder(ACTIVATION_URL.format(_ENV_ID_)) + .returnResponse { request, i -> + FlagshipTestsHelper.response("", 200) + } + .build() + ) + + FlagshipTestsHelper.interceptor() + .addRule(FlagshipTestsHelper.HttpInterceptor.Rule.Builder(CONTEXT_URL.format(_ENV_ID_)) + .returnResponse { request, i -> + FlagshipTestsHelper.response("", 200) + } + .build()) + FlagshipTestsHelper.interceptor() + .addRule( + FlagshipTestsHelper.HttpInterceptor.Rule.Builder(CAMPAIGNS_URL.format(_ENV_ID_)) + .returnResponse( + FlagshipTestsHelper.responseFromAssets( + ApplicationProvider.getApplicationContext(), + "api_response_1.json", + 200 + ) + ) + .build() + ) + val warningList = ArrayList() + Flagship.start(getApplication(), "MY_ENV_ID", "MY_API_KEY", FlagshipConfig.DecisionApi() + .withLogLevel(LogManager.Level.WARNING) + .withLogManager(object : LogManager() { + override fun onLog(level: Level, tag: String, message: String) { + if (level == Level.WARNING) + warningList.add(message) + } + })) + Thread.sleep(100) + runBlocking { + val visitor = Flagship.newVisitor("visitor_abcd") + .build() + visitor.getFlag("one", 1) // 1W created + visitor.fetchFlags().await() + visitor.getFlag("one", 1) // 0W Updated + visitor.authenticate("visitor_abcd") + visitor.getFlag("one", 1) // 0W same visitor + visitor.authenticate("visitor_1234") + visitor.getFlag("one", 1) // 1W authenticated + visitor.unauthenticate() + visitor.getFlag("one", 1) // 1W unauthenticated + visitor.authenticate("visitor_5678") + visitor.fetchFlags().await() + visitor.getFlag("one", 1) // 0W uptodate + visitor.updateContext("age", 33) + visitor.getFlag("one", 1) // 1W context + visitor.getFlag("one", 1) // 1W context + visitor.fetchFlags().await() + visitor.updateContext("age", 33) + visitor.getFlag("one", 1) // 0W same context + visitor.updateContext("age", 34) + visitor.getFlag("one", 1) // 1W context + visitor.fetchFlags().await() + visitor.getFlag("one", 1) // 0W Uptodate + } + assertEquals(6, warningList.size) + assertEquals(FlagshipConstants.Warnings.FLAGS_CREATED.format("visitor_abcd"), warningList[0]) + assertEquals(FlagshipConstants.Warnings.FLAGS_AUTHENTICATED.format("visitor_1234"), warningList[1]) + assertEquals(FlagshipConstants.Warnings.FLAGS_UNAUTHENTICATED.format("visitor_abcd"), warningList[2]) + assertEquals(FlagshipConstants.Warnings.FLAGS_CONTEXT_UPDATED.format("visitor_5678"), warningList[3]) + assertEquals(FlagshipConstants.Warnings.FLAGS_CONTEXT_UPDATED.format("visitor_5678"), warningList[4]) + assertEquals(FlagshipConstants.Warnings.FLAGS_CONTEXT_UPDATED.format("visitor_5678"), warningList[5]) + } } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a2da8db..6b4615b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Jun 15 19:32:02 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From d7504382b4e09b96427551f52eb76f3524f03c4c Mon Sep 17 00:00:00 2001 From: raf-abtasty Date: Fri, 24 Jan 2025 12:08:47 +0100 Subject: [PATCH 17/70] Feature/fs3 1530 eai (#9) * Fs3 563 event value type (#9) Fix Event Hit value type: must be > to 0 * Test/GitHub package (#10) **Changes:** - Compilation and publishing to Maven Central * start tracking manager * hits refactor, need http refactor * loop is ok * up to date * Feature/fs3 1034 fetch warning (#11) - **Changed** - userExposed has been renamed to visitorExposed - **Added** - Flagmetadata now returns campaign name, variation group name and variation name. - onVisitorExposed callback - Warning when Flags need to be updated * start new cache manager * fix new cache interfaces * merge * change db schema + interfaces * hit from cache to test * start tests * to clean and start QA * rework tf, cache and hits * tf flags, cache, visitor * move to async * QA TM * to finish XPC tests * rework logs + tests ok * consent is now mandatory at visitor creation * fix Response Compat read body * removed compatr * Trust isrg-root-X1 cert * ok * cleaned * clean 2 * status and tests ok * add missing files * ok for statuses * ok for FlagCollection * TS enablement to test * End of TS continue TS TF * test ok for troubleshooting * ok * usage tf * start collect * collect almost ok, need to check touches data * ok collect need refacto * need fix TU +clean+log * also strategy * ok for tests * added strategy + some logs * clean and retest * fix timeout * start TF EAI, fix build * first TF * more tf * tf * code cleaned * rename collectEmotionsAIEvents * account settings troubleshooting, add eai troubleshooting, beta version * Troubleshooting ok * fix tf and coverage with jacoco * enabled tf logs on CI * disabled tf logs on CI * test ci * fix tf and ci * fix tf add gradle build * tf class 1 by 1 * tf class 1 by 1 * tf class 1 by 1 * tf class 1 by 1 * tf class 1 by 1 * try with more delay and multitask * try gradle options * try gradle options * try gradle options * try gradle options * try gradle options * try gradle options * try gradle options * try gradle options * try gradle options * try with splited commands test * try with splited commands test 2 * gradle cache * test without continuous_fail * try cache * try gradle only build flagship --- .github/workflows/ci-unitest-build.yml | 89 +- .github/workflows/release.yml | 9 +- app/build.gradle | 43 +- app/src/main/AndroidManifest.xml | 15 +- .../com/abtasty/flagshipqa/FirstFragment.kt | 44 + .../com/abtasty/flagshipqa/MainActivity.kt | 19 + .../com/abtasty/flagshipqa/MainActivity2.kt | 41 + .../com/abtasty/flagshipqa/MainActivity3.kt | 206 ++ .../com/abtasty/flagshipqa/SecondFragment.kt | 44 + .../flagshipqa/ui/context/ContextViewModel.kt | 2 +- .../flagshipqa/ui/dashboard/ConfigFragment.kt | 6 +- .../ui/dashboard/ConfigViewModel.kt | 44 +- .../flagshipqa/ui/events/EventFragment.kt | 6 + .../ui/modifications/ModificationFragment.kt | 23 +- .../ui/modifications/ModificationViewModel.kt | 24 +- .../com/abtasty/flagshipqa/ui/theme/Color.kt | 11 + .../com/abtasty/flagshipqa/ui/theme/Theme.kt | 58 + .../com/abtasty/flagshipqa/ui/theme/Type.kt | 34 + .../flagshipqa/ui/visitor/VisitorViewModel.kt | 2 +- .../drawable/baseline_arrow_forward_24.xml | 5 + app/src/main/res/layout/activity_main2.xml | 33 + app/src/main/res/layout/content_main.xml | 19 + app/src/main/res/layout/fragment_event.xml | 23 + app/src/main/res/layout/fragment_first.xml | 35 + app/src/main/res/layout/fragment_second.xml | 35 + app/src/main/res/menu/bottom_nav_menu.xml | 1 - app/src/main/res/navigation/nav_graph.xml | 28 + app/src/main/res/values-land/dimens.xml | 3 + app/src/main/res/values-night/themes.xml | 7 + app/src/main/res/values-v23/themes.xml | 9 + app/src/main/res/values-w1240dp/dimens.xml | 3 + app/src/main/res/values-w600dp/dimens.xml | 3 + app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 38 + app/src/main/res/values/themes.xml | 11 + build.gradle | 15 +- flagship/build.gradle | 70 +- flagship/flagship-publishing.gradle | 24 +- flagship/jacoco.gradle | 29 +- .../2.json | 20 +- .../3.json | 72 + .../3.json | 72 + .../com/abtasty/flagship/api/HttpCompat.kt | 45 - .../abtasty/flagship/api/ResponseCompat.kt | 32 - .../com/abtasty/flagship/api/HttpManager.kt | 114 +- .../flagship/api/HttpResponseCompat.kt | 23 + .../flagship/api/IFlagshipEndpoints.kt | 14 +- .../abtasty/flagship/api/TrackingManager.kt | 957 ++++++++- .../abtasty/flagship/cache/CacheManager.kt | 94 +- .../flagship/cache/DefaultCacheManager.kt | 188 +- .../abtasty/flagship/cache/HitCacheHelper.kt | 137 +- .../flagship/cache/IHitCacheImplementation.kt | 18 +- .../flagship/cache/VisitorCacheHelper.kt | 25 +- .../flagship/database/DefaultDatabase.kt | 38 +- .../java/com/abtasty/flagship/database/Hit.kt | 4 +- .../com/abtasty/flagship/database/HitDao.kt | 23 +- .../abtasty/flagship/database/VisitorDao.kt | 22 +- .../abtasty/flagship/decision/ApiManager.kt | 86 +- .../flagship/decision/BucketingManager.kt | 44 +- .../flagship/decision/DecisionManager.kt | 75 +- .../flagship/decision/IDecisionManager.kt | 2 + .../flagship/eai/EAIGestureListener.kt | 94 + .../com/abtasty/flagship/eai/EAIManager.kt | 599 ++++++ .../abtasty/flagship/eai/EAIWindowCallBack.kt | 117 + .../com/abtasty/flagship/eai/OnEAIEvents.kt | 7 + .../eai/OnWindowDispatchTouchEvent.kt | 7 + .../com/abtasty/flagship/hits/Activate.kt | 75 +- .../java/com/abtasty/flagship/hits/Batch.kt | 67 +- .../java/com/abtasty/flagship/hits/Consent.kt | 45 +- .../flagship/hits/DeveloperUsageTracking.kt | 14 + .../java/com/abtasty/flagship/hits/Event.kt | 20 +- .../java/com/abtasty/flagship/hits/Hit.kt | 166 +- .../java/com/abtasty/flagship/hits/Item.kt | 28 +- .../java/com/abtasty/flagship/hits/Page.kt | 32 +- .../java/com/abtasty/flagship/hits/Screen.kt | 16 +- .../java/com/abtasty/flagship/hits/Segment.kt | 27 + .../com/abtasty/flagship/hits/Transaction.kt | 27 +- .../abtasty/flagship/hits/TroubleShooting.kt | 671 ++++++ .../java/com/abtasty/flagship/hits/Usage.kt | 73 + .../com/abtasty/flagship/hits/VisitorEvent.kt | 37 + .../abtasty/flagship/main/ConfigManager.kt | 81 +- .../com/abtasty/flagship/main/Flagship.kt | 163 +- .../abtasty/flagship/main/FlagshipConfig.kt | 40 +- .../com/abtasty/flagship/model/ExposedFlag.kt | 37 + .../java/com/abtasty/flagship/model/Flag.kt | 70 +- .../abtasty/flagship/model/FlagCollection.kt | 97 + .../abtasty/flagship/model/FlagMetadata.kt | 28 + .../abtasty/flagship/model/Modifications.kt | 4 +- .../com/abtasty/flagship/model/Variation.kt | 5 +- .../java/com/abtasty/flagship/model/_Flag.kt | 6 + .../flagship/utils/EFlagFetchingStatus.kt | 9 - .../utils/FetchFlagsRequiredStatusReason.kt | 11 + .../com/abtasty/flagship/utils/FlagStatus.kt | 35 + .../flagship/utils/FlagshipConstants.kt | 214 +- .../abtasty/flagship/utils/FlagshipContext.kt | 57 +- .../flagship/utils/FlagshipLogManager.kt | 12 +- .../com/abtasty/flagship/utils}/HttpCompat.kt | 2 +- .../flagship/utils/OnFetchStatusChanged.kt | 22 + .../abtasty/flagship/utils}/ResponseCompat.kt | 13 +- .../java/com/abtasty/flagship/utils/Utils.kt | 193 ++ .../flagship/visitor/DefaultStrategy.kt | 401 ++-- .../com/abtasty/flagship/visitor/IVisitor.kt | 24 +- .../flagship/visitor/NoConsentStrategy.kt | 17 +- .../flagship/visitor/NotReadyStrategy.kt | 32 +- .../abtasty/flagship/visitor/PanicStrategy.kt | 24 +- .../com/abtasty/flagship/visitor/Visitor.kt | 123 +- .../flagship/visitor/VisitorDelegate.kt | 45 +- .../flagship/visitor/VisitorDelegateDTO.kt | 13 +- .../flagship/visitor/VisitorExposed.kt | 29 +- .../flagship/visitor/VisitorStrategy.kt | 49 +- flagship/src/main/res/layout/main.xml | 8 + flagship/src/main/res/values/colors.xml | 20 + flagship/src/main/res/values/strings.xml | 2 + flagship/src/main/res/values/styles.xml | 10 + .../assets/account_settings_collect_only.json | 5 + .../test/assets/account_settings_full.json | 7 + .../test/assets/account_settings_no_eai.json | 5 + flagship/src/test/assets/api_response_2.json | 138 ++ flagship/src/test/assets/api_response_3.json | 121 ++ .../src/test/assets/bucketing_response_2.json | 84 + .../src/test/assets/bucketing_response_3.json | 319 +++ flagship/src/test/assets/cache_hit.json | 56 +- flagship/src/test/assets/cache_hit_1.json | 18 + flagship/src/test/assets/cache_hit_2.json | 19 + .../cache_visitor_with_eai_scored_only.json | 54 + .../src/test/assets/uc-info_response_1.json | 5 + .../com/abtasty/flagship/AFlagshipTest.kt | 97 + .../com/abtasty/flagship/FlagshipTests.kt | 1817 ---------------- .../abtasty/flagship/FlagshipTestsCache.kt | 511 +++++ .../abtasty/flagship/FlagshipTestsConfig.kt | 137 ++ .../com/abtasty/flagship/FlagshipTestsEAI.kt | 797 +++++++ .../abtasty/flagship/FlagshipTestsFlags.kt | 451 ++++ .../abtasty/flagship/FlagshipTestsHelper.kt | 169 +- .../com/abtasty/flagship/FlagshipTestsHits.kt | 1883 +++++++++++++++++ .../flagship/FlagshipTestsTrackingManager.kt | 1608 ++++++++++++++ .../abtasty/flagship/FlagshipTestsVisitor.kt | 810 +++++++ .../flagship/FlagshipTestsVisitorStrategy.kt | 307 +++ gradle.properties | 3 + gradle/wrapper/gradle-wrapper.properties | 2 +- 139 files changed, 13518 insertions(+), 3140 deletions(-) create mode 100644 app/src/main/java/com/abtasty/flagshipqa/FirstFragment.kt create mode 100644 app/src/main/java/com/abtasty/flagshipqa/MainActivity2.kt create mode 100644 app/src/main/java/com/abtasty/flagshipqa/MainActivity3.kt create mode 100644 app/src/main/java/com/abtasty/flagshipqa/SecondFragment.kt create mode 100644 app/src/main/java/com/abtasty/flagshipqa/ui/theme/Color.kt create mode 100644 app/src/main/java/com/abtasty/flagshipqa/ui/theme/Theme.kt create mode 100644 app/src/main/java/com/abtasty/flagshipqa/ui/theme/Type.kt create mode 100644 app/src/main/res/drawable/baseline_arrow_forward_24.xml create mode 100644 app/src/main/res/layout/activity_main2.xml create mode 100644 app/src/main/res/layout/content_main.xml create mode 100644 app/src/main/res/layout/fragment_first.xml create mode 100644 app/src/main/res/layout/fragment_second.xml create mode 100644 app/src/main/res/navigation/nav_graph.xml create mode 100644 app/src/main/res/values-land/dimens.xml create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values-v23/themes.xml create mode 100644 app/src/main/res/values-w1240dp/dimens.xml create mode 100644 app/src/main/res/values-w600dp/dimens.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 flagship/schemas/com.abtasty.flagship.database.DefaultDatabase/3.json create mode 100644 flagship/schemas/com.abtasty.flagship.database.IRoomDatabaseMigration/3.json delete mode 100644 flagship/src/compat/java/com/abtasty/flagship/api/HttpCompat.kt delete mode 100644 flagship/src/compat/java/com/abtasty/flagship/api/ResponseCompat.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/eai/EAIGestureListener.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/eai/EAIManager.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/eai/EAIWindowCallBack.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/eai/OnEAIEvents.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/eai/OnWindowDispatchTouchEvent.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/hits/DeveloperUsageTracking.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/hits/Segment.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/hits/TroubleShooting.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/hits/Usage.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/hits/VisitorEvent.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/model/ExposedFlag.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/model/FlagCollection.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/model/_Flag.kt delete mode 100644 flagship/src/main/java/com/abtasty/flagship/utils/EFlagFetchingStatus.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/utils/FetchFlagsRequiredStatusReason.kt create mode 100644 flagship/src/main/java/com/abtasty/flagship/utils/FlagStatus.kt rename flagship/src/{common/java/com/abtasty/flagship/api => main/java/com/abtasty/flagship/utils}/HttpCompat.kt (97%) create mode 100644 flagship/src/main/java/com/abtasty/flagship/utils/OnFetchStatusChanged.kt rename flagship/src/{common/java/com/abtasty/flagship/api => main/java/com/abtasty/flagship/utils}/ResponseCompat.kt (68%) create mode 100644 flagship/src/main/java/com/abtasty/flagship/utils/Utils.kt create mode 100644 flagship/src/main/res/layout/main.xml create mode 100644 flagship/src/main/res/values/colors.xml create mode 100644 flagship/src/main/res/values/styles.xml create mode 100644 flagship/src/test/assets/account_settings_collect_only.json create mode 100644 flagship/src/test/assets/account_settings_full.json create mode 100644 flagship/src/test/assets/account_settings_no_eai.json create mode 100644 flagship/src/test/assets/api_response_2.json create mode 100644 flagship/src/test/assets/api_response_3.json create mode 100644 flagship/src/test/assets/bucketing_response_2.json create mode 100644 flagship/src/test/assets/bucketing_response_3.json create mode 100644 flagship/src/test/assets/cache_hit_1.json create mode 100644 flagship/src/test/assets/cache_hit_2.json create mode 100644 flagship/src/test/assets/cache_visitor_with_eai_scored_only.json create mode 100644 flagship/src/test/assets/uc-info_response_1.json create mode 100644 flagship/src/test/java/com/abtasty/flagship/AFlagshipTest.kt delete mode 100644 flagship/src/test/java/com/abtasty/flagship/FlagshipTests.kt create mode 100644 flagship/src/test/java/com/abtasty/flagship/FlagshipTestsCache.kt create mode 100644 flagship/src/test/java/com/abtasty/flagship/FlagshipTestsConfig.kt create mode 100644 flagship/src/test/java/com/abtasty/flagship/FlagshipTestsEAI.kt create mode 100644 flagship/src/test/java/com/abtasty/flagship/FlagshipTestsFlags.kt create mode 100644 flagship/src/test/java/com/abtasty/flagship/FlagshipTestsHits.kt create mode 100644 flagship/src/test/java/com/abtasty/flagship/FlagshipTestsTrackingManager.kt create mode 100644 flagship/src/test/java/com/abtasty/flagship/FlagshipTestsVisitor.kt create mode 100644 flagship/src/test/java/com/abtasty/flagship/FlagshipTestsVisitorStrategy.kt diff --git a/.github/workflows/ci-unitest-build.yml b/.github/workflows/ci-unitest-build.yml index 32ff40e..5436255 100644 --- a/.github/workflows/ci-unitest-build.yml +++ b/.github/workflows/ci-unitest-build.yml @@ -9,23 +9,102 @@ on: branches: [ master ] pull_request: branches: [ master ] + workflow_dispatch: + jobs: test: name: Run Unit Tests runs-on: ubuntu-latest - if: contains(github.event.head_commit.message, '#ci-auto') == false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: "oracle" java-version: '17' +# - name: Setup Android SDK +# uses: android-actions/setup-android@v3 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + gradle-version: 8.7 + cache-read-only: true + - name: Clean + run: ./gradlew clean + - name: Build with Gradle + run: ./gradlew :flagship:assembleDebug + + - name: Unit tests - run: bash ./gradlew flagship:testAllVariantsWithCoverage + run: bash ./gradlew flagship:testDebugUnitTestCoverage -i --stacktrace +# - name: Generate report +# run: bash ./gradlew flagship:createDebugUnitTestCoverageReport + ##### +# - name: Test FlagshipTestsCache +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsCache --debug +# - name: Test FlagshipTestsConfig +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsConfig --debug +# - name: Test FlagshipTestsEAI.kt +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsEAI --debug +# - name: Test FlagshipTestsFlags +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsFlags --debug +# - name: Test FlagshipTestsVisitor +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsVisitor --debug +# - name: Test FlagshipTestsVisitorStrategy +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsVisitorStrategy --debug +# - name: Test FlagshipTestsTrackingManager +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager --debug +# - name: Test FlagshipTestsTrackingManager.test_tracking_manager_continuous +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager.test_tracking_manager_continuous --debug + +# - name: Test FlagshipTestsTrackingManager.test_tracking_manager_continuous_fail +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager.test_tracking_manager_continuous_fail --debug + +# - name: Test FlagshipTestsTrackingManager.test_tracking_manager_continuous_intervals +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager.test_tracking_manager_continuous_intervals --debug +# +# - name: Test FlagshipTestsTrackingManager.test_tracking_manager_continuous_max_pool +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager.test_tracking_manager_continuous_max_pool --debug +# +# - name: Test FlagshipTestsTrackingManager.test_tracking_manager_continuous_panic +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager.test_tracking_manager_continuous_panic --debug +# +# - name: Test FlagshipTestsTrackingManager.test_tracking_manager_no_batching_main +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager.test_tracking_manager_no_batching_main --debug +# +# - name: Test FlagshipTestsTrackingManager.test_tracking_manager_no_batching_no_consent +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager.test_tracking_manager_no_batching_no_consent --debug +# +# - name: Test FlagshipTestsTrackingManager.test_tracking_manager_no_batching_restart +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager.test_tracking_manager_no_batching_restart --debug +# +# - name: Test FlagshipTestsTrackingManager.test_tracking_manager_periodic_activate_fail +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager.test_tracking_manager_periodic_activate_fail --debug +# +# - name: Test FlagshipTestsTrackingManager.test_tracking_manager_periodic_batch_fail +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager.test_tracking_manager_periodic_batch_fail --debug +# +# - name: Test FlagshipTestsTrackingManager.test_tracking_manager_periodic_bucketing +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager.test_tracking_manager_periodic_bucketing --debug +# +# - name: Test FlagshipTestsTrackingManager.test_tracking_manager_periodic_consent +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager.test_tracking_manager_periodic_consent --debug +# +# - name: Test FlagshipTestsTrackingManager.test_tracking_manager_periodic_hit_fail +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager.test_tracking_manager_periodic_hit_fail --debug +# +# - name: Test FlagshipTestsTrackingManager.test_tracking_manager_no_batching_bucketing +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager.test_tracking_manager_no_batching_bucketing --debug +# +# - name: Test FlagshipTestsTrackingManager.test_tracking_manager_periodic_xpc +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsTrackingManager.test_tracking_manager_periodic_xpc --debug + +# - name: Test FlagshipTestsHits +# run: bash ./gradlew :flagship:testDebugUnitTest --tests com.abtasty.flagship.FlagshipTestsHits --debug + - name: Upload coverage to codecov uses: codecov/codecov-action@v2 with: - files: flagship/build/reports/jacoco/testCommonDebugUnitTestCoverage/testCommonDebugUnitTestCoverage.xml + files: flagship/build/reports/coverage/test/debug/index.xml - name: Build run: bash ./gradlew flagship:clean flagship:assembleRelease diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57402dc..c876ec4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,12 +17,15 @@ jobs: distribution: "oracle" java-version: "17" - name: Unit tests - run: bash ./gradlew clean flagship:testAllVariantsWithCoverage + run: bash ./gradlew clean flagship:testJacocoUnitTestCoverage - name: Get version run: | echo "FLAGSHIP_VERSION_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - - name: Upload Test Report - run: bash <(curl -s https://codecov.io/bash) -f "flagship/build/reports/jacoco/testCommonDebugUnitTestCoverage/testCommonDebugUnitTestCoverage.xml" + - name: Upload coverage to codecov + uses: codecov/codecov-action@v2 + with: + files: + flagship/build/jacocoHtml/index.xml - name: Build and Publish env: SONATYPE_SIGNING_KEY: ${{ secrets.SONATYPE_SIGNING_KEY }} diff --git a/app/build.gradle b/app/build.gradle index 0820283..3e6e293 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,7 +7,6 @@ keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) android { compileSdk 34 - signingConfigs { release { keyAlias keystoreProperties['RELEASE_KEY_ALIAS'] @@ -19,6 +18,7 @@ android { buildFeatures { viewBinding true + compose true } @@ -35,6 +35,9 @@ android { viewBinding { enabled = true } + vectorDrawables { + useSupportLibrary true + } } buildTypes { @@ -45,25 +48,22 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { - jvmTarget = '17' + jvmTarget = '1.8' } - flavorDimensions 'default' - productFlavors { - common { - dimension 'default' - minSdkVersion 21 - } - compat { - dimension 'default' - minSdkVersion 16 + namespace 'com.abtasty.flagshipqa' + composeOptions { + kotlinCompilerExtensionVersion '1.5.15' + } + packaging { + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' } } - namespace 'com.abtasty.flagshipqa' } dependencies { @@ -79,12 +79,23 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.10" - implementation project(path: ':flagship') //Use local project -// implementation 'com.abtasty:flagship-android:3.0.5' //Use remote maven repository + implementation project(path: ':flagship') + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7' + implementation 'androidx.activity:activity-compose:1.8.0' + implementation platform('androidx.compose:compose-bom:2024.04.01') + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.ui:ui-graphics' + implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.compose.material3:material3'//Use local project +// implementation 'com.abtasty:flagship-android:4.0.0' //Use remote maven repository testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation platform('androidx.compose:compose-bom:2024.04.01') + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + debugImplementation 'androidx.compose.ui:ui-tooling' + debugImplementation 'androidx.compose.ui:ui-test-manifest' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f0ee414..fc8d1f4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ - + + + + android:exported="true" + android:label="@string/app_name"> diff --git a/app/src/main/java/com/abtasty/flagshipqa/FirstFragment.kt b/app/src/main/java/com/abtasty/flagshipqa/FirstFragment.kt new file mode 100644 index 0000000..fb9473a --- /dev/null +++ b/app/src/main/java/com/abtasty/flagshipqa/FirstFragment.kt @@ -0,0 +1,44 @@ +package com.abtasty.flagshipqa + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import com.abtasty.flagshipqa.databinding.FragmentFirstBinding + +/** + * A simple [Fragment] subclass as the default destination in the navigation. + */ +class FirstFragment : Fragment() { + + private var _binding: FragmentFirstBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + + _binding = FragmentFirstBinding.inflate(inflater, container, false) + return binding.root + + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.buttonFirst.setOnClickListener { + findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ 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 b013b0d..bc6aca0 100644 --- a/app/src/main/java/com/abtasty/flagshipqa/MainActivity.kt +++ b/app/src/main/java/com/abtasty/flagshipqa/MainActivity.kt @@ -1,12 +1,25 @@ 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() { @@ -31,4 +44,10 @@ class MainActivity : AppCompatActivity() { setupActionBarWithNavController(navController, appBarConfiguration) navView.setupWithNavController(navController) } + + override fun onResume() { + super.onResume() +// val intent = Intent(this, MainActivity3::class.java) +// startActivity(intent) + } } \ No newline at end of file diff --git a/app/src/main/java/com/abtasty/flagshipqa/MainActivity2.kt b/app/src/main/java/com/abtasty/flagshipqa/MainActivity2.kt new file mode 100644 index 0000000..ad8e389 --- /dev/null +++ b/app/src/main/java/com/abtasty/flagshipqa/MainActivity2.kt @@ -0,0 +1,41 @@ +package com.abtasty.flagshipqa + +import android.os.Bundle +import com.google.android.material.snackbar.Snackbar +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.navigateUp +import androidx.navigation.ui.setupActionBarWithNavController +import com.abtasty.flagshipqa.databinding.ActivityMain2Binding + +class MainActivity2 : AppCompatActivity() { + + private lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var binding: ActivityMain2Binding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityMain2Binding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.toolbar) + + val navController = findNavController(R.id.nav_host_fragment_content_main) + appBarConfiguration = AppBarConfiguration(navController.graph) + setupActionBarWithNavController(navController, appBarConfiguration) + + binding.fab.setOnClickListener { view -> + Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) + .setAction("Action", null) + .setAnchorView(R.id.fab).show() + } + } + + override fun onSupportNavigateUp(): Boolean { + val navController = findNavController(R.id.nav_host_fragment_content_main) + return navController.navigateUp(appBarConfiguration) + || super.onSupportNavigateUp() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/abtasty/flagshipqa/MainActivity3.kt b/app/src/main/java/com/abtasty/flagshipqa/MainActivity3.kt new file mode 100644 index 0000000..8edb02f --- /dev/null +++ b/app/src/main/java/com/abtasty/flagshipqa/MainActivity3.kt @@ -0,0 +1,206 @@ +package com.abtasty.flagshipqa + +import android.os.Build +import android.os.Bundle +import android.view.ActionMode +import android.view.KeyEvent +import android.view.Menu +import android.view.MenuItem +import android.view.MotionEvent +import android.view.SearchEvent +import android.view.View +import android.view.Window +import android.view.WindowManager +import android.view.accessibility.AccessibilityEvent +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import com.abtasty.flagship.main.Flagship +import com.abtasty.flagship.main.FlagshipConfig +import com.abtasty.flagshipqa.ui.theme.FlagshipandroidTheme +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.delay + + +class MainActivity3 : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + + val window = window + val windowCallback = window.callback + val callbackWrapper: WindowCallback = WindowCallback(windowCallback) + window.callback = callbackWrapper + +// CoroutineScope(Dispatchers.Default).async { +// delay(5000) + Flagship.start( + application, + "", + "", + FlagshipConfig.DecisionApi() + ) + +// } + enableEdgeToEdge() + setContent { + FlagshipandroidTheme { + Scaffold(modifier = Modifier.fillMaxSize(), containerColor = Color.Cyan) { innerPadding -> + Greeting( + name = "Android", + modifier = Modifier.padding(innerPadding) + ) + } + } + } + } + + override fun onResume() { + super.onResume() + + Flagship.runOnFlagshipIsInitialized { + val visitor = Flagship.newVisitor("toto_89edfe742qesq", true).build() + visitor.collectEmotionsAIEvents(this@MainActivity3) + } + } +} + +@Composable +fun Greeting(name: String, modifier: Modifier = Modifier) { + var switchValue by remember { mutableStateOf(true) } + Column { + Text( + text = "Hello $name!", + modifier = modifier + ) + + Switch(checked = switchValue, onCheckedChange = { value-> + switchValue = value + }) + } +} + +@Preview(showBackground = true) +@Composable +fun GreetingPreview() { + FlagshipandroidTheme { + Greeting("Android") + } +} + + +public class WindowCallback(var wrapper: Window.Callback): Window.Callback { + + + override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + return wrapper.dispatchKeyEvent(event) + } + + override fun dispatchKeyShortcutEvent(event: KeyEvent?): Boolean { + return wrapper.dispatchKeyShortcutEvent(event) + } + + override fun dispatchTouchEvent(event: MotionEvent?): Boolean { + System.out.println("APP > dispatch touch event") + return wrapper.dispatchTouchEvent(event) + } + + override fun dispatchTrackballEvent(event: MotionEvent?): Boolean { + return wrapper.dispatchTrackballEvent(event) + } + + override fun dispatchGenericMotionEvent(event: MotionEvent?): Boolean { + return wrapper.dispatchGenericMotionEvent(event) + } + + override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent?): Boolean { + return wrapper.dispatchPopulateAccessibilityEvent(event) + } + + override fun onCreatePanelView(featureId: Int): View? { + return wrapper.onCreatePanelView(featureId) + } + + override fun onCreatePanelMenu(featureId: Int, menu: Menu): Boolean { + return wrapper.onCreatePanelMenu(featureId, menu) + } + + override fun onPreparePanel(featureId: Int, view: View?, menu: Menu): Boolean { + return wrapper.onPreparePanel(featureId, view, menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + return wrapper.onMenuOpened(featureId, menu) + } + + override fun onMenuItemSelected(featureId: Int, item: MenuItem): Boolean { + return wrapper.onMenuItemSelected(featureId, item) + } + + override fun onWindowAttributesChanged(attrs: WindowManager.LayoutParams?) { + return wrapper.onWindowAttributesChanged(attrs) + } + + override fun onContentChanged() { + return wrapper.onContentChanged() + } + + override fun onWindowFocusChanged(hasFocus: Boolean) { + return wrapper.onWindowFocusChanged(hasFocus) + } + + override fun onAttachedToWindow() { + return wrapper.onAttachedToWindow() + } + + override fun onDetachedFromWindow() { + return wrapper.onDetachedFromWindow() + } + + override fun onPanelClosed(featureId: Int, menu: Menu) { + return wrapper.onPanelClosed(featureId, menu) + } + + override fun onSearchRequested(): Boolean { + return wrapper.onSearchRequested() + } + + @RequiresApi(Build.VERSION_CODES.M) + override fun onSearchRequested(searchEvent: SearchEvent?): Boolean { + return wrapper.onSearchRequested(searchEvent) + } + + override fun onWindowStartingActionMode(callback: ActionMode.Callback?): ActionMode? { + return wrapper.onWindowStartingActionMode(callback) + } + + @RequiresApi(Build.VERSION_CODES.M) + override fun onWindowStartingActionMode(callback: ActionMode.Callback?, type: Int): ActionMode? { + return wrapper.onWindowStartingActionMode(callback, type) + } + + override fun onActionModeStarted(mode: ActionMode?) { + return wrapper.onActionModeStarted(mode) + } + + override fun onActionModeFinished(mode: ActionMode?) { + return wrapper.onActionModeFinished(mode) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/abtasty/flagshipqa/SecondFragment.kt b/app/src/main/java/com/abtasty/flagshipqa/SecondFragment.kt new file mode 100644 index 0000000..5939bb1 --- /dev/null +++ b/app/src/main/java/com/abtasty/flagshipqa/SecondFragment.kt @@ -0,0 +1,44 @@ +package com.abtasty.flagshipqa + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import com.abtasty.flagshipqa.databinding.FragmentSecondBinding + +/** + * A simple [Fragment] subclass as the second destination in the navigation. + */ +class SecondFragment : Fragment() { + + private var _binding: FragmentSecondBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + + _binding = FragmentSecondBinding.inflate(inflater, container, false) + return binding.root + + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.buttonSecond.setOnClickListener { + findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/abtasty/flagshipqa/ui/context/ContextViewModel.kt b/app/src/main/java/com/abtasty/flagshipqa/ui/context/ContextViewModel.kt index f71d478..91eb859 100644 --- a/app/src/main/java/com/abtasty/flagshipqa/ui/context/ContextViewModel.kt +++ b/app/src/main/java/com/abtasty/flagshipqa/ui/context/ContextViewModel.kt @@ -45,7 +45,7 @@ class ContextViewModel(val appContext: Application) : AndroidViewModel(appContex val context = getVisitorContext(error) Flagship.getVisitor()?.clearContext() Flagship.getVisitor()?.updateContext(context) - Flagship.getVisitor()?.synchronizeModifications()?.invokeOnCompletion { + Flagship.getVisitor()?.fetchFlags()?.invokeOnCompletion { Handler(Looper.getMainLooper()).post { success(appContext.resources.getString(R.string.fragment_context_success)) } diff --git a/app/src/main/java/com/abtasty/flagshipqa/ui/dashboard/ConfigFragment.kt b/app/src/main/java/com/abtasty/flagshipqa/ui/dashboard/ConfigFragment.kt index 10e394c..eafe055 100644 --- a/app/src/main/java/com/abtasty/flagshipqa/ui/dashboard/ConfigFragment.kt +++ b/app/src/main/java/com/abtasty/flagshipqa/ui/dashboard/ConfigFragment.kt @@ -108,12 +108,14 @@ class ConfigFragment : Fragment() { } fun startFlagship() { - dashboardViewModel.startFlagship( - { + { visitor -> activity?.runOnUiThread { Toast.makeText(requireContext(), "Started", Toast.LENGTH_SHORT).show() } + println("#DB here 1: " + Thread.currentThread().name) + visitor.collectEmotionsAIEvents(activity) + println("#DB here 2: " + Thread.currentThread().name) }, { error -> Toast.makeText(requireContext(), "Error : $error", Toast.LENGTH_SHORT).show() 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 243a9a8..daaf9ab 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 @@ -6,14 +6,14 @@ import android.os.Bundle import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import com.abtasty.flagship.cache.CacheManager -import com.abtasty.flagship.cache.IHitCacheImplementation -import com.abtasty.flagship.cache.IVisitorCacheImplementation +import com.abtasty.flagship.api.CacheStrategy +import com.abtasty.flagship.api.TrackingManagerConfig import com.abtasty.flagship.main.Flagship import com.abtasty.flagship.main.FlagshipConfig import com.abtasty.flagship.utils.LogManager +import com.abtasty.flagship.visitor.Visitor import com.abtasty.flagshipqa.R -import org.json.JSONArray +import kotlinx.coroutines.runBlocking import org.json.JSONObject import java.util.concurrent.TimeUnit @@ -71,7 +71,7 @@ class ConfigViewModel(val appContext: Application) : AndroidViewModel(appContext pollingIntervalUnit.value = sharedPreferences.getString("pollingUnit", "MS") } - fun startFlagship(ready: () -> Unit, error: (message: String) -> Unit) { + fun startFlagship(ready: (Visitor) -> Unit, error: (message: String) -> Unit) { val errorStr = checkParamError() if (errorStr.isNotEmpty()) error(errorStr) @@ -81,12 +81,15 @@ class ConfigViewModel(val appContext: Application) : AndroidViewModel(appContext flagshipConfig.withPollingIntervals(pollingIntervalTime.value!!, getPollingIntervalUnit()) flagshipConfig.withTimeout(timeout.value ?: 2000) flagshipConfig.withLogLevel(LogManager.Level.ALL) - flagshipConfig.withStatusListener { status -> - if (status == Flagship.Status.READY) { - createVisitor(); - ready() + flagshipConfig.withFlagshipStatusListener { status -> + if (status == Flagship.FlagshipStatus.INITIALIZED || status == Flagship.FlagshipStatus.PANIC) { + println("#DB STATUS INITIALIZED") + val visitor = createVisitor() + ready(visitor) } } + flagshipConfig.withTrackingManagerConfig(TrackingManagerConfig(maxPoolSize = 5, batchTimeInterval = 20000, disablePolling = false, cachingStrategy = CacheStrategy.PERIODIC_CACHING)) +// flagshipConfig.withTrackingManagerConfig(TrackingManagerConfig(maxPoolSize = 5, batchTimeInterval = 20000, cachingStrategy = CacheStrategy.CONTINUOUS_CACHING)) flagshipConfig.withOnVisitorExposed { visitorExposed, exposedFlag -> System.out.println("[OnVisitorExposed] : " + visitorExposed.visitorId + " \n" + "key: " + exposedFlag.key + "\n" @@ -95,7 +98,22 @@ class ConfigViewModel(val appContext: Application) : AndroidViewModel(appContext + "variation name: " + exposedFlag.metadata.variationName ) } - Flagship.start(getApplication(), env_id.value!!, api_key.value!!, flagshipConfig.build()) + // + flagshipConfig.withTrackingManagerConfig( + TrackingManagerConfig( + maxPoolSize = 5, + batchTimeInterval = 10000 + ) + ) + // + runBlocking { + Flagship.start( + getApplication(), + env_id.value!!, + api_key.value!!, + flagshipConfig.build() + ).await() + } } } @@ -110,13 +128,13 @@ class ConfigViewModel(val appContext: Application) : AndroidViewModel(appContext } } - fun createVisitor() { + fun createVisitor(): Visitor { val visitorContext = getVisitorContext() - Flagship.newVisitor(visitorId.value.toString()) + val visitor = Flagship.newVisitor(visitorId.value.toString(), hasConsented.value!!) .isAuthenticated(isAuthenticated.value!!) - .hasConsented(hasConsented.value!!) .context(visitorContext) .build() + return visitor } fun getStringResource(id: Int): String { diff --git a/app/src/main/java/com/abtasty/flagshipqa/ui/events/EventFragment.kt b/app/src/main/java/com/abtasty/flagshipqa/ui/events/EventFragment.kt index f1b1083..47307e0 100644 --- a/app/src/main/java/com/abtasty/flagshipqa/ui/events/EventFragment.kt +++ b/app/src/main/java/com/abtasty/flagshipqa/ui/events/EventFragment.kt @@ -1,5 +1,6 @@ package com.abtasty.flagshipqa.ui.events +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -7,6 +8,7 @@ import android.view.ViewGroup import android.widget.ArrayAdapter import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import com.abtasty.flagshipqa.MainActivity2 import com.abtasty.flagshipqa.R import com.abtasty.flagshipqa.databinding.FragmentEventBinding @@ -48,6 +50,10 @@ class EventFragment : Fragment() { binding.editTextProductSku.text.toString() ) } + binding.buttonNextActivity.setOnClickListener { + val intent = Intent(this.context, MainActivity2::class.java) + startActivity(intent) + } return binding.root } } \ No newline at end of file diff --git a/app/src/main/java/com/abtasty/flagshipqa/ui/modifications/ModificationFragment.kt b/app/src/main/java/com/abtasty/flagshipqa/ui/modifications/ModificationFragment.kt index 1ca1d36..666b150 100644 --- a/app/src/main/java/com/abtasty/flagshipqa/ui/modifications/ModificationFragment.kt +++ b/app/src/main/java/com/abtasty/flagshipqa/ui/modifications/ModificationFragment.kt @@ -50,15 +50,22 @@ class ModificationFragment : Fragment() { ) } - modificationViewModel.value.observe(viewLifecycleOwner, Observer { - binding.editTextResultValue.setText(it.toString()) - }) +// modificationViewModel.value.observe(viewLifecycleOwner, Observer { +// binding.editTextResultValue.setText(it?.toString() ?: "null") +// }) +// +// modificationViewModel.info.observe(viewLifecycleOwner, Observer { +//// root.edit_text_result_campaign.setText(it.optString("campaignId", "unknown")) +//// root.edit_text_result_group.setText(it.optString("variationGroupId", "unknown")) +//// root.edit_text_result_variation.setText(it.optString("variationId", "unknown")) +// binding.editTextInfo.setText(it.toString(4)) +// }) - modificationViewModel.info.observe(viewLifecycleOwner, Observer { -// root.edit_text_result_campaign.setText(it.optString("campaignId", "unknown")) -// root.edit_text_result_group.setText(it.optString("variationGroupId", "unknown")) -// root.edit_text_result_variation.setText(it.optString("variationId", "unknown")) - binding.editTextInfo.setText(it.toString(4)) + modificationViewModel.currentFLag.observe(viewLifecycleOwner, Observer { + val type = binding.spinner.selectedItem.toString() + val default = binding.editTextDefault.text.toString() + binding.editTextResultValue.setText((it?.value(modificationViewModel.getTypedValue(type, default),false) ?: "null").toString()) + binding.editTextInfo.setText(it?.metadata()?.toJson()?.toString(4)) }) binding.activate.setOnClickListener { 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 56335ee..d3a51b6 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 @@ -5,6 +5,7 @@ import android.widget.Toast 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 @@ -17,10 +18,11 @@ import kotlin.reflect.jvm.isAccessible class ModificationViewModel(val appContext: Application) : AndroidViewModel(appContext) { - var types = arrayListOf("Boolean", "String", "Number", "Json") + var types = arrayListOf("Boolean", "String", "Number", "Json", "Null") var modifications = MutableLiveData().apply { this.value = "{\n\n}" } - var value = MutableLiveData().apply { this.value = "" } - var info = MutableLiveData().apply { this.value = JSONObject() } +// var value = MutableLiveData().apply { this.value = "" } +// var info = MutableLiveData().apply { this.value = JSONObject() } + var currentFLag = MutableLiveData().apply { this.value = null } init { @@ -54,8 +56,9 @@ class ModificationViewModel(val appContext: Application) : AndroidViewModel(appC modifications.value = json.toString(4) } - fun getTypedValue(type: String, default : String) : Any { + fun getTypedValue(type: String, default : String) : Any? { return when (type) { + "Null" -> null "String" -> default "Boolean" -> default.toLowerCase().toBoolean() "Number" -> { @@ -87,16 +90,21 @@ class ModificationViewModel(val appContext: Application) : AndroidViewModel(appC fun getModification(key: String, default: String, type: String) { Flagship.getVisitor()?.let { visitor -> - value.value = visitor.getModification(key, getTypedValue(type, default)) - info.value = visitor.getFlag(key, getTypedValue(type, default)).metadata().toJson() +// value.value = visitor.getModification(key, getTypedValue(type, default)) +// value.value = visitor.getFlag(key).value(getTypedValue(type, default),false) +// info.value = visitor.getFlag(key).metadata().toJson() + currentFLag.value = visitor.getFlag(key) } } fun activate(key: String, default: String, type: String) { // Flagship.getVisitor()?.activateModification(key) - Flagship.getVisitor()?.let { visitor -> - visitor.getFlag(key, getTypedValue(type, default)).visitorExposed() +// Flagship.getVisitor()?.let { visitor -> +// visitor.getFlag(key).visitorExposed() +// } + if (currentFLag.value != null) { + (currentFLag.value as Flag).visitorExposed() } Toast.makeText(appContext, "Activation sent", Toast.LENGTH_SHORT).show(); } diff --git a/app/src/main/java/com/abtasty/flagshipqa/ui/theme/Color.kt b/app/src/main/java/com/abtasty/flagshipqa/ui/theme/Color.kt new file mode 100644 index 0000000..461714c --- /dev/null +++ b/app/src/main/java/com/abtasty/flagshipqa/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.abtasty.flagshipqa.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/abtasty/flagshipqa/ui/theme/Theme.kt b/app/src/main/java/com/abtasty/flagshipqa/ui/theme/Theme.kt new file mode 100644 index 0000000..097de0b --- /dev/null +++ b/app/src/main/java/com/abtasty/flagshipqa/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.abtasty.flagshipqa.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun FlagshipandroidTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/abtasty/flagshipqa/ui/theme/Type.kt b/app/src/main/java/com/abtasty/flagshipqa/ui/theme/Type.kt new file mode 100644 index 0000000..2daefca --- /dev/null +++ b/app/src/main/java/com/abtasty/flagshipqa/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.abtasty.flagshipqa.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/java/com/abtasty/flagshipqa/ui/visitor/VisitorViewModel.kt b/app/src/main/java/com/abtasty/flagshipqa/ui/visitor/VisitorViewModel.kt index 8584eab..e4167df 100644 --- a/app/src/main/java/com/abtasty/flagshipqa/ui/visitor/VisitorViewModel.kt +++ b/app/src/main/java/com/abtasty/flagshipqa/ui/visitor/VisitorViewModel.kt @@ -60,7 +60,7 @@ class VisitorViewModel(val appContext: Application) : AndroidViewModel(appContex // updateIds() // } - Flagship.getVisitor()?.synchronizeModifications()?.invokeOnCompletion { + Flagship.getVisitor()?.fetchFlags()?.invokeOnCompletion { GlobalScope.launch { withContext(Dispatchers.Main) { diff --git a/app/src/main/res/drawable/baseline_arrow_forward_24.xml b/app/src/main/res/drawable/baseline_arrow_forward_24.xml new file mode 100644 index 0000000..38ac379 --- /dev/null +++ b/app/src/main/res/drawable/baseline_arrow_forward_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_main2.xml b/app/src/main/res/layout/activity_main2.xml new file mode 100644 index 0000000..4f3cfb2 --- /dev/null +++ b/app/src/main/res/layout/activity_main2.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml new file mode 100644 index 0000000..e416e1c --- /dev/null +++ b/app/src/main/res/layout/content_main.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_event.xml b/app/src/main/res/layout/fragment_event.xml index c731956..035e691 100644 --- a/app/src/main/res/layout/fragment_event.xml +++ b/app/src/main/res/layout/fragment_event.xml @@ -301,6 +301,29 @@ app:layout_constraintStart_toStartOf="@+id/edit_text_interface" app:layout_constraintTop_toBottomOf="@+id/edit_text_product_sku" /> + + +