Skip to content

Commit

Permalink
allow contributing from modules to matching attributes mappers (#4386)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/1202552961248957/1207001823978864/f 
Api proposal change: https://app.asana.com/0/1202552961248957/1207001823978859/f

### Description
The PR allows modules to contribute with matching attribute mappers to the remote messaging processor.

As part of the new api we are introducing a new matching attribute that makes use of new api.

### Steps to test this PR

_Feature 1_
- [x] checkout branch
- [x] go to RemoteMessagingService and change the url with `https://jsonblob.com/api/jsonBlob/1225382942368194560`
- [x] fresh install
- [x] skip onboarding
- [x] you shouldn't see any RMF in the new tab
- [x] now create a sync account
- [x] Go to https://www.jsonblob.com/1225382942368194560 and bump version +1 and click save
- [x] restart app (using fire button or killing it)
- [x] you should now see a remote message in the new tab

### UI changes
| Before  | After |
| ------ | ----- |
!(Upload before screenshot)|(Upload after screenshot)|
  • Loading branch information
cmonfortep authored Apr 9, 2024
1 parent 4d756eb commit 3b3e908
Show file tree
Hide file tree
Showing 20 changed files with 277 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,19 @@
package com.duckduckgo.remote.messaging.api

interface AttributeMatcherPlugin {
suspend fun evaluate(matchingAttribute: MatchingAttribute<*>): Boolean?
suspend fun evaluate(matchingAttribute: MatchingAttribute): Boolean?
}

interface MatchingAttribute<T> {
fun matches(matchingValue: T): Boolean?
interface MatchingAttribute

interface JsonToMatchingAttributeMapper {
fun map(key: String, jsonMatchingAttribute: JsonMatchingAttribute): MatchingAttribute?
}

data class JsonMatchingAttribute(
val value: Any? = null,
val min: Any? = null,
val max: Any? = null,
val since: Any? = null,
val fallback: Boolean? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class RemoteMessagingConfigMatcher(
return null
}

private suspend fun Iterable<Int>.evaluateMatchingRules(rules: Map<Int, List<MatchingAttribute<*>>>): EvaluationResult {
private suspend fun Iterable<Int>.evaluateMatchingRules(rules: Map<Int, List<MatchingAttribute>>): EvaluationResult {
var result: EvaluationResult = EvaluationResult.Match

for (rule in this) {
Expand All @@ -68,7 +68,7 @@ class RemoteMessagingConfigMatcher(
return result
}

private suspend fun Iterable<Int>.evaluateExclusionRules(rules: Map<Int, List<MatchingAttribute<*>>>): EvaluationResult {
private suspend fun Iterable<Int>.evaluateExclusionRules(rules: Map<Int, List<MatchingAttribute>>): EvaluationResult {
var result: EvaluationResult = EvaluationResult.Fail

for (rule in this) {
Expand All @@ -89,7 +89,7 @@ class RemoteMessagingConfigMatcher(
return result
}

private suspend fun evaluateAttribute(matchingAttribute: MatchingAttribute<*>): EvaluationResult {
private suspend fun evaluateAttribute(matchingAttribute: MatchingAttribute): EvaluationResult {
if (matchingAttribute is Unknown) {
return matchingAttribute.fallback.toResult()
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.DaggerSet
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.remote.messaging.api.AttributeMatcherPlugin
import com.duckduckgo.remote.messaging.api.JsonToMatchingAttributeMapper
import com.duckduckgo.remote.messaging.api.MessageActionMapperPlugin
import com.duckduckgo.remote.messaging.api.RemoteMessagingRepository
import com.duckduckgo.remote.messaging.impl.*
Expand Down Expand Up @@ -104,10 +105,11 @@ object DataSourceModule {
@Provides
@SingleInstanceIn(AppScope::class)
fun providesRemoteMessagingConfigJsonMapper(
matchingAttributeMappers: DaggerSet<JsonToMatchingAttributeMapper>,
actionMappers: DaggerSet<MessageActionMapperPlugin>,
appBuildConfig: AppBuildConfig,
): RemoteMessagingConfigJsonMapper {
return RemoteMessagingConfigJsonMapper(appBuildConfig, actionMappers)
return RemoteMessagingConfigJsonMapper(appBuildConfig, matchingAttributeMappers, actionMappers)
}

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.duckduckgo.remote.messaging.impl.mappers

import com.duckduckgo.remote.messaging.api.JsonMatchingAttribute
import com.duckduckgo.remote.messaging.api.JsonToMatchingAttributeMapper
import com.duckduckgo.remote.messaging.api.MatchingAttribute
import com.duckduckgo.remote.messaging.impl.models.*
import com.duckduckgo.remote.messaging.impl.models.toIntOrDefault
Expand All @@ -27,14 +29,14 @@ import timber.log.Timber
private val dateFormatter = SimpleDateFormat("yyyy-mm-dd")

@Suppress("UNCHECKED_CAST")
private val localeMapper: (JsonMatchingAttribute) -> MatchingAttribute<String> = { jsonMatchingAttribute ->
private val localeMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
Locale(
value = jsonMatchingAttribute.value.toStringList(),
fallback = jsonMatchingAttribute.fallback,
)
}

private val osApiMapper: (JsonMatchingAttribute) -> MatchingAttribute<Int> = { jsonMatchingAttribute ->
private val osApiMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
Api(
value = jsonMatchingAttribute.value.toIntOrDefault(MATCHING_ATTR_INT_DEFAULT_VALUE),
min = jsonMatchingAttribute.min.toIntOrDefault(MATCHING_ATTR_INT_DEFAULT_VALUE),
Expand All @@ -43,7 +45,7 @@ private val osApiMapper: (JsonMatchingAttribute) -> MatchingAttribute<Int> = { j
)
}

private val webViewMapper: (JsonMatchingAttribute) -> MatchingAttribute<String> = { jsonMatchingAttribute ->
private val webViewMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
WebView(
value = jsonMatchingAttribute.value.toStringOrDefault(MATCHING_ATTR_STRING_DEFAULT_VALUE),
min = jsonMatchingAttribute.min.toStringOrDefault(MATCHING_ATTR_STRING_DEFAULT_VALUE),
Expand All @@ -53,21 +55,21 @@ private val webViewMapper: (JsonMatchingAttribute) -> MatchingAttribute<String>
}

@Suppress("UNCHECKED_CAST")
private val flavorMapper: (JsonMatchingAttribute) -> MatchingAttribute<String> = { jsonMatchingAttribute ->
private val flavorMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
Flavor(
value = jsonMatchingAttribute.value.toStringList(),
fallback = jsonMatchingAttribute.fallback,
)
}

private val appIdMapper: (JsonMatchingAttribute) -> MatchingAttribute<String> = { jsonMatchingAttribute ->
private val appIdMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
AppId(
value = jsonMatchingAttribute.value.toStringOrDefault(MATCHING_ATTR_STRING_DEFAULT_VALUE),
fallback = jsonMatchingAttribute.fallback,
)
}

private val appVersionMapper: (JsonMatchingAttribute) -> MatchingAttribute<String> = { jsonMatchingAttribute ->
private val appVersionMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
AppVersion(
value = jsonMatchingAttribute.value.toStringOrDefault(MATCHING_ATTR_STRING_DEFAULT_VALUE),
min = jsonMatchingAttribute.min.toStringOrDefault(MATCHING_ATTR_STRING_DEFAULT_VALUE),
Expand All @@ -76,77 +78,77 @@ private val appVersionMapper: (JsonMatchingAttribute) -> MatchingAttribute<Strin
)
}

private val atbMapper: (JsonMatchingAttribute) -> MatchingAttribute<String> = { jsonMatchingAttribute ->
private val atbMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
Atb(
value = jsonMatchingAttribute.value.toStringOrDefault(MATCHING_ATTR_STRING_DEFAULT_VALUE),
fallback = jsonMatchingAttribute.fallback,
)
}

private val appAtbMapper: (JsonMatchingAttribute) -> MatchingAttribute<String> = { jsonMatchingAttribute ->
private val appAtbMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
AppAtb(
value = jsonMatchingAttribute.value.toStringOrDefault(MATCHING_ATTR_STRING_DEFAULT_VALUE),
fallback = jsonMatchingAttribute.fallback,
)
}

private val searchAtbMapper: (JsonMatchingAttribute) -> MatchingAttribute<String> = { jsonMatchingAttribute ->
private val searchAtbMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
SearchAtb(
value = jsonMatchingAttribute.value.toStringOrDefault(MATCHING_ATTR_STRING_DEFAULT_VALUE),
fallback = jsonMatchingAttribute.fallback,
)
}

private val expVariantMapper: (JsonMatchingAttribute) -> MatchingAttribute<String> = { jsonMatchingAttribute ->
private val expVariantMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
ExpVariant(
value = jsonMatchingAttribute.value.toStringOrDefault(MATCHING_ATTR_STRING_DEFAULT_VALUE),
fallback = jsonMatchingAttribute.fallback,
)
}

private val installedGPlayMapper: (JsonMatchingAttribute) -> MatchingAttribute<Boolean> = { jsonMatchingAttribute ->
private val installedGPlayMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
InstalledGPlay(
value = jsonMatchingAttribute.value as Boolean,
fallback = jsonMatchingAttribute.fallback,
)
}

private val defaultBrowserMapper: (JsonMatchingAttribute) -> MatchingAttribute<Boolean> = { jsonMatchingAttribute ->
private val defaultBrowserMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
DefaultBrowser(
value = jsonMatchingAttribute.value as Boolean,
fallback = jsonMatchingAttribute.fallback,
)
}

private val emailEnabledMapper: (JsonMatchingAttribute) -> MatchingAttribute<Boolean> = { jsonMatchingAttribute ->
private val emailEnabledMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
EmailEnabled(
value = jsonMatchingAttribute.value as Boolean,
fallback = jsonMatchingAttribute.fallback,
)
}

private val appTrackingProtectionOnboarded: (JsonMatchingAttribute) -> MatchingAttribute<Boolean> = { jsonMatchingAttribute ->
private val appTrackingProtectionOnboarded: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
AppTpOnboarded(
value = jsonMatchingAttribute.value as Boolean,
fallback = jsonMatchingAttribute.fallback,
)
}

private val networkProtectionOnboarded: (JsonMatchingAttribute) -> MatchingAttribute<Boolean> = { jsonMatchingAttribute ->
private val networkProtectionOnboarded: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
NetPOnboarded(
value = jsonMatchingAttribute.value as Boolean,
fallback = jsonMatchingAttribute.fallback,
)
}

private val widgetAddedMapper: (JsonMatchingAttribute) -> MatchingAttribute<Boolean> = { jsonMatchingAttribute ->
private val widgetAddedMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
WidgetAdded(
value = jsonMatchingAttribute.value as Boolean,
fallback = jsonMatchingAttribute.fallback,
)
}

private val searchCountMapper: (JsonMatchingAttribute) -> MatchingAttribute<Int> = { jsonMatchingAttribute ->
private val searchCountMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
SearchCount(
value = jsonMatchingAttribute.value.toIntOrDefault(MATCHING_ATTR_INT_DEFAULT_VALUE),
min = jsonMatchingAttribute.min.toIntOrDefault(MATCHING_ATTR_INT_DEFAULT_VALUE),
Expand All @@ -155,7 +157,7 @@ private val searchCountMapper: (JsonMatchingAttribute) -> MatchingAttribute<Int>
)
}

private val bookmarksMapper: (JsonMatchingAttribute) -> MatchingAttribute<Int> = { jsonMatchingAttribute ->
private val bookmarksMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
Bookmarks(
value = jsonMatchingAttribute.value.toIntOrDefault(MATCHING_ATTR_INT_DEFAULT_VALUE),
min = jsonMatchingAttribute.min.toIntOrDefault(MATCHING_ATTR_INT_DEFAULT_VALUE),
Expand All @@ -164,7 +166,7 @@ private val bookmarksMapper: (JsonMatchingAttribute) -> MatchingAttribute<Int> =
)
}

private val favoritesMapper: (JsonMatchingAttribute) -> MatchingAttribute<Int> = { jsonMatchingAttribute ->
private val favoritesMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
Favorites(
value = jsonMatchingAttribute.value.toIntOrDefault(MATCHING_ATTR_INT_DEFAULT_VALUE),
min = jsonMatchingAttribute.min.toIntOrDefault(MATCHING_ATTR_INT_DEFAULT_VALUE),
Expand All @@ -173,14 +175,14 @@ private val favoritesMapper: (JsonMatchingAttribute) -> MatchingAttribute<Int> =
)
}

private val appThemeMapper: (JsonMatchingAttribute) -> MatchingAttribute<String> = { jsonMatchingAttribute ->
private val appThemeMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
AppTheme(
value = jsonMatchingAttribute.value.toStringOrDefault(MATCHING_ATTR_STRING_DEFAULT_VALUE),
fallback = jsonMatchingAttribute.fallback,
)
}

private val daysSinceInstalledMapper: (JsonMatchingAttribute) -> MatchingAttribute<Int> = { jsonMatchingAttribute ->
private val daysSinceInstalledMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
DaysSinceInstalled(
value = jsonMatchingAttribute.value.toIntOrDefault(MATCHING_ATTR_INT_DEFAULT_VALUE),
min = jsonMatchingAttribute.min.toIntOrDefault(MATCHING_ATTR_INT_DEFAULT_VALUE),
Expand All @@ -189,7 +191,7 @@ private val daysSinceInstalledMapper: (JsonMatchingAttribute) -> MatchingAttribu
)
}

private val daysUsedSinceMapper: (JsonMatchingAttribute) -> MatchingAttribute<Int> = { jsonMatchingAttribute ->
private val daysUsedSinceMapper: (JsonMatchingAttribute) -> MatchingAttribute = { jsonMatchingAttribute ->
DaysUsedSince(
since = dateFormatter.parse(jsonMatchingAttribute.since as String)!!,
value = jsonMatchingAttribute.value.toIntOrDefault(MATCHING_ATTR_INT_DEFAULT_VALUE),
Expand Down Expand Up @@ -223,12 +225,18 @@ private val attributesMappers = mapOf(
Pair("netpOnboarded", networkProtectionOnboarded),
)

fun List<JsonMatchingRule>.mapToMatchingRules(): Map<Int, List<MatchingAttribute<*>>> = this.map {
Pair(it.id, it.attributes.map { attrs -> attrs.map() })
fun List<JsonMatchingRule>.mapToMatchingRules(
matchingAttributeMappers: Set<JsonToMatchingAttributeMapper>,
): Map<Int, List<MatchingAttribute>> = this.map {
Pair(it.id, it.attributes.map { attrs -> attrs.map(matchingAttributeMappers) })
}.toMap()

private fun Map.Entry<String, JsonMatchingAttribute>.map(): MatchingAttribute<*> {
private fun Map.Entry<String, JsonMatchingAttribute>.map(matchingAttributeMappers: Set<JsonToMatchingAttributeMapper>): MatchingAttribute {
return runCatching {
matchingAttributeMappers.forEach {
val matchingAttribute = it.map(this.key, this.value)
if (matchingAttribute != null) return@runCatching matchingAttribute
}
attributesMappers[this.key]?.invoke(this.value) ?: Unknown(this.value.fallback)
}.onFailure {
Timber.i("RMF: error $it")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,21 @@
package com.duckduckgo.remote.messaging.impl.mappers

import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.remote.messaging.api.JsonToMatchingAttributeMapper
import com.duckduckgo.remote.messaging.api.MessageActionMapperPlugin
import com.duckduckgo.remote.messaging.impl.models.JsonRemoteMessagingConfig
import com.duckduckgo.remote.messaging.impl.models.RemoteConfig
import timber.log.Timber

class RemoteMessagingConfigJsonMapper(
private val appBuildConfig: AppBuildConfig,
private val matchingAttributeMappers: Set<JsonToMatchingAttributeMapper>,
private val actionMappers: Set<MessageActionMapperPlugin>,
) {
fun map(jsonRemoteMessagingConfig: JsonRemoteMessagingConfig): RemoteConfig {
val messages = jsonRemoteMessagingConfig.messages.mapToRemoteMessage(appBuildConfig.deviceLocale, actionMappers)
Timber.i("RMF: messages parsed $messages")
val rules = jsonRemoteMessagingConfig.rules.mapToMatchingRules()
val rules = jsonRemoteMessagingConfig.rules.mapToMatchingRules(matchingAttributeMappers)
return RemoteConfig(
messages = messages,
rules = rules,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class AndroidAppAttributeMatcher(
private val appProperties: AppProperties,
private val appBuildConfig: AppBuildConfig,
) : AttributeMatcherPlugin {
override suspend fun evaluate(matchingAttribute: MatchingAttribute<*>): Boolean? {
override suspend fun evaluate(matchingAttribute: MatchingAttribute): Boolean? {
return when (matchingAttribute) {
is Flavor -> {
matchingAttribute.matches(appBuildConfig.flavor.toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class DeviceAttributeMatcher(
private val appBuildConfig: AppBuildConfig,
private val appProperties: AppProperties,
) : AttributeMatcherPlugin {
override suspend fun evaluate(matchingAttribute: MatchingAttribute<*>): Boolean? {
override suspend fun evaluate(matchingAttribute: MatchingAttribute): Boolean? {
return when (matchingAttribute) {
is Api -> {
matchingAttribute.matches(appBuildConfig.sdkInt)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import com.duckduckgo.remote.messaging.impl.models.*
class UserAttributeMatcher(
private val userBrowserProperties: UserBrowserProperties,
) : AttributeMatcherPlugin {
override suspend fun evaluate(matchingAttribute: MatchingAttribute<*>): Boolean? {
override suspend fun evaluate(matchingAttribute: MatchingAttribute): Boolean? {
return when (matchingAttribute) {
is AppTheme -> {
matchingAttribute.matches(userBrowserProperties.appTheme().toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.duckduckgo.remote.messaging.impl.models

import com.duckduckgo.remote.messaging.api.JsonMatchingAttribute
import com.duckduckgo.remote.messaging.api.JsonMessageAction

data class JsonRemoteMessagingConfig(
Expand Down Expand Up @@ -59,14 +60,6 @@ data class JsonMatchingRule(
val attributes: Map<String, JsonMatchingAttribute>,
)

data class JsonMatchingAttribute(
val value: Any? = null,
val min: Any? = null,
val max: Any? = null,
val since: Any? = null,
val fallback: Boolean? = null,
)

sealed class JsonMessageType(val jsonValue: String) {
object SMALL : JsonMessageType("small")
object MEDIUM : JsonMessageType("medium")
Expand Down
Loading

0 comments on commit 3b3e908

Please sign in to comment.