Skip to content

Commit 416cbb5

Browse files
committed
Merge branch 'hotfix/5.210.1'
2 parents 23f4868 + fd53891 commit 416cbb5

File tree

26 files changed

+640
-36
lines changed

26 files changed

+640
-36
lines changed

anvil/anvil-annotations/src/main/java/com/duckduckgo/anvil/annotations/ContributesActivePlugin.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,16 @@ annotation class ContributesActivePlugin(
6060
* The [ContributesActivePlugin] coalesce both
6161
*/
6262
val priority: Int = 0,
63+
64+
/**
65+
* When `true` the backing feature flag supports experimentation. Otherwise it will be a regular feature flag.
66+
*
67+
* When `true` the (generated) backing feature flag is annotated with the [Experiment] annotation (read its JavaDoc)
68+
*/
69+
val supportExperiments: Boolean = false,
70+
71+
/**
72+
* When `true` the backing feature flag will ALWAYS be enabled for internal builds, regardless of remote config or default value.
73+
*/
74+
val internalAlwaysEnabled: Boolean = false,
6375
)

anvil/anvil-compiler/src/main/java/com/duckduckgo/anvil/compiler/ContributesActivePluginPointCodeGenerator.kt

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import com.duckduckgo.anvil.annotations.ContributesRemoteFeature
2323
import com.duckduckgo.anvil.annotations.PriorityKey
2424
import com.duckduckgo.feature.toggles.api.RemoteFeatureStoreNamed
2525
import com.duckduckgo.feature.toggles.api.Toggle
26+
import com.duckduckgo.feature.toggles.api.Toggle.Experiment
27+
import com.duckduckgo.feature.toggles.api.Toggle.InternalAlwaysEnabled
2628
import com.google.auto.service.AutoService
2729
import com.squareup.anvil.annotations.ContributesBinding
2830
import com.squareup.anvil.annotations.ContributesMultibinding
@@ -314,6 +316,12 @@ class ContributesActivePluginPointCodeGenerator : CodeGenerator {
314316
val pluginRemoteFeatureClassName = "${vmClass.shortName}_ActivePlugin_RemoteFeature"
315317
val pluginRemoteFeatureStoreClassName = "${vmClass.shortName}_ActivePlugin_RemoteFeature_MultiProcessStore"
316318
val pluginPriority = vmClass.annotations.firstOrNull { it.fqName == ContributesActivePlugin::class.fqName }?.priorityOrNull()
319+
val pluginSupportExperiments = vmClass.annotations.firstOrNull {
320+
it.fqName == ContributesActivePlugin::class.fqName
321+
}?.isExperimentOrNull() ?: false
322+
val pluginInternalAlwaysEnabled = vmClass.annotations.firstOrNull {
323+
it.fqName == ContributesActivePlugin::class.fqName
324+
}?.internalAlwaysEnabledOrNull() ?: false
317325

318326
// Check if there's another plugin class, in the same plugin point, that has the same class simplename
319327
// we can't allow that because the backing remote feature would be the same
@@ -403,14 +411,24 @@ class ContributesActivePluginPointCodeGenerator : CodeGenerator {
403411
.build(),
404412
)
405413
addFunction(
406-
FunSpec.builder(featureName)
407-
.addModifiers(ABSTRACT)
408-
.addAnnotation(
414+
FunSpec.builder(featureName).apply {
415+
addModifiers(ABSTRACT)
416+
addAnnotation(
409417
AnnotationSpec.builder(Toggle.DefaultValue::class)
410418
.addMember("defaultValue = %L", featureDefaultValue)
411419
.build(),
412420
)
413-
.returns(Toggle::class)
421+
// If the active plugin defines [supportExperiments = true] we mark it as Experiment
422+
if (pluginSupportExperiments) {
423+
addAnnotation(AnnotationSpec.builder(Experiment::class).build())
424+
}
425+
// If the active plugin defines [internalAlwaysEnabled = true] we mark it as InternalAlwaysEnabled
426+
if (pluginInternalAlwaysEnabled) {
427+
addAnnotation(AnnotationSpec.builder(InternalAlwaysEnabled::class).build())
428+
}
429+
430+
returns(Toggle::class)
431+
}
414432
.build(),
415433
)
416434
}.build(),
@@ -579,6 +597,12 @@ class ContributesActivePluginPointCodeGenerator : CodeGenerator {
579597
@OptIn(ExperimentalAnvilApi::class)
580598
private fun AnnotationReference.priorityOrNull(): Int? = argumentAt("priority", 3)?.value()
581599

600+
@OptIn(ExperimentalAnvilApi::class)
601+
private fun AnnotationReference.isExperimentOrNull(): Boolean? = argumentAt("supportExperiments", 4)?.value()
602+
603+
@OptIn(ExperimentalAnvilApi::class)
604+
private fun AnnotationReference.internalAlwaysEnabledOrNull(): Boolean? = argumentAt("internalAlwaysEnabled", 5)?.value()
605+
582606
private fun ClassReference.Psi.pluginClassName(
583607
fqName: FqName,
584608
): ClassName? {

app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3551,13 +3551,15 @@ class BrowserTabFragment :
35513551
if (viewState.showSuggestions || viewState.showFavorites) {
35523552
if (viewState.favorites.isNotEmpty() && viewState.showFavorites) {
35533553
showFocusedView()
3554+
viewModel.autoCompleteSuggestionsGone()
35543555
binding.autoCompleteSuggestionsList.gone()
35553556
} else {
35563557
binding.autoCompleteSuggestionsList.show()
35573558
autoCompleteSuggestionsAdapter.updateData(viewState.searchResults.query, viewState.searchResults.suggestions)
35583559
hideFocusedView()
35593560
}
35603561
} else {
3562+
viewModel.autoCompleteSuggestionsGone()
35613563
binding.autoCompleteSuggestionsList.gone()
35623564
hideFocusedView()
35633565
}

app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedViewProvider.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class RealFocusedViewProvider @Inject constructor(
5050
scope = ActivityScope::class,
5151
boundType = FocusedViewPlugin::class,
5252
priority = 100,
53+
supportExperiments = true,
5354
)
5455
class FocusedLegacyPage @Inject constructor() : FocusedViewPlugin {
5556

@@ -65,6 +66,8 @@ class FocusedLegacyPage @Inject constructor() : FocusedViewPlugin {
6566
boundType = FocusedViewPlugin::class,
6667
priority = 0,
6768
defaultActiveValue = false,
69+
supportExperiments = true,
70+
internalAlwaysEnabled = true,
6871
)
6972
class FocusedPage @Inject constructor() : FocusedViewPlugin {
7073

app/src/main/java/com/duckduckgo/app/browser/tabpreview/TabEntityDiffCallback.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import com.duckduckgo.app.tabs.model.TabEntity
2222

2323
class TabEntityDiffCallback(
2424
private val oldList: List<TabEntity>,
25-
var newList: List<TabEntity>,
25+
private val newList: List<TabEntity>,
2626
) : DiffUtil.Callback() {
2727

2828
private fun areItemsTheSame(

app/src/main/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptor.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ object PixelInterceptorPixelsRequiringDataCleaning : PixelParamRemovalPlugin {
8383
StatisticsPixelName.BROWSER_DAILY_ACTIVE_FEATURE_STATE.pixelName to PixelParameter.removeAll(),
8484
WebViewPixelName.WEB_PAGE_LOADED.pixelName to PixelParameter.removeAll(),
8585
WebViewPixelName.WEB_PAGE_PAINTED.pixelName to PixelParameter.removeAll(),
86+
AppPixelName.REFERRAL_INSTALL_UTM_CAMPAIGN.pixelName to PixelParameter.removeAtb(),
8687
)
8788
}
8889
}

app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,4 +347,6 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName {
347347
TAB_MANAGER_REARRANGE_BANNER_DISPLAYED("m_tab_manager_rearrange_banner_displayed"),
348348

349349
ADD_BOOKMARK_CONFIRM_EDITED("m_add_bookmark_confirm_edit"),
350+
351+
REFERRAL_INSTALL_UTM_CAMPAIGN("m_android_install"),
350352
}

app/src/main/java/com/duckduckgo/app/referral/AppInstallationReferrerParser.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,18 @@ interface AppInstallationReferrerParser {
3232

3333
@Suppress("SameParameterValue")
3434
@ContributesBinding(AppScope::class)
35-
class QueryParamReferrerParser @Inject constructor() : AppInstallationReferrerParser {
35+
class QueryParamReferrerParser @Inject constructor(
36+
private val originAttributeHandler: ReferrerOriginAttributeHandler,
37+
) : AppInstallationReferrerParser {
3638

3739
override fun parse(referrer: String): ParsedReferrerResult {
40+
Timber.v("Full referrer string: %s", referrer)
41+
3842
val referrerParts = splitIntoConstituentParts(referrer)
39-
if (referrerParts.isNullOrEmpty()) return ReferrerNotFound(fromCache = false)
43+
if (referrerParts.isEmpty()) return ReferrerNotFound(fromCache = false)
44+
45+
// processing this doesn't change anything with the ATB-based campaign referrer or EU search/ballot logic
46+
originAttributeHandler.process(referrerParts)
4047

4148
val auctionReferrer = extractEuAuctionReferrer(referrerParts)
4249
if (auctionReferrer is EuAuctionSearchChoiceReferrerFound || auctionReferrer is EuAuctionBrowserChoiceReferrerFound) {
@@ -60,7 +67,7 @@ class QueryParamReferrerParser @Inject constructor() : AppInstallationReferrerPa
6067
}
6168
}
6269

63-
Timber.d("App not installed as a result of EU auction")
70+
Timber.d("No EU referrer data found; app not installed as a result of EU auction or choice screen")
6471
return ReferrerNotFound()
6572
}
6673

@@ -101,8 +108,8 @@ class QueryParamReferrerParser @Inject constructor() : AppInstallationReferrerPa
101108
return fullCampaignName.substringAfter(prefix, "")
102109
}
103110

104-
private fun splitIntoConstituentParts(referrer: String?): List<String>? {
105-
return referrer?.split("&")
111+
private fun splitIntoConstituentParts(referrer: String?): List<String> {
112+
return referrer?.split("&") ?: emptyList()
106113
}
107114

108115
companion object {
@@ -124,7 +131,7 @@ sealed class ParsedReferrerResult(open val fromCache: Boolean = false) {
124131

125132
data class ReferrerNotFound(override val fromCache: Boolean = false) : ParsedReferrerResult(fromCache)
126133
data class ParseFailure(val reason: ParseFailureReason) : ParsedReferrerResult()
127-
object ReferrerInitialising : ParsedReferrerResult()
134+
data object ReferrerInitialising : ParsedReferrerResult()
128135
}
129136

130137
sealed class ParseFailureReason {

app/src/main/java/com/duckduckgo/app/referral/AppReferrerDataStore.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ interface AppReferrerDataStore {
2828
var referrerCheckedPreviously: Boolean
2929
var campaignSuffix: String?
3030
var installedFromEuAuction: Boolean
31+
var utmOriginAttributeCampaign: String?
3132
}
3233

3334
@ContributesBinding(AppScope::class)
@@ -37,6 +38,10 @@ class AppReferenceSharePreferences @Inject constructor(private val context: Cont
3738
get() = preferences.getString(KEY_CAMPAIGN_SUFFIX, null)
3839
set(value) = preferences.edit(true) { putString(KEY_CAMPAIGN_SUFFIX, value) }
3940

41+
override var utmOriginAttributeCampaign: String?
42+
get() = preferences.getString(KEY_ORIGIN_ATTRIBUTE_CAMPAIGN, null)
43+
set(value) = preferences.edit(true) { putString(KEY_ORIGIN_ATTRIBUTE_CAMPAIGN, value) }
44+
4045
override var referrerCheckedPreviously: Boolean
4146
get() = preferences.getBoolean(KEY_CHECKED_PREVIOUSLY, false)
4247
set(value) = preferences.edit(true) { putBoolean(KEY_CHECKED_PREVIOUSLY, value) }
@@ -50,6 +55,7 @@ class AppReferenceSharePreferences @Inject constructor(private val context: Cont
5055
companion object {
5156
const val FILENAME = "com.duckduckgo.app.referral"
5257
private const val KEY_CAMPAIGN_SUFFIX = "KEY_CAMPAIGN_SUFFIX"
58+
private const val KEY_ORIGIN_ATTRIBUTE_CAMPAIGN = "KEY_ORIGIN_ATTRIBUTE_CAMPAIGN"
5359
private const val KEY_CHECKED_PREVIOUSLY = "KEY_CHECKED_PREVIOUSLY"
5460
private const val KEY_INSTALLED_FROM_EU_AUCTION = "KEY_INSTALLED_FROM_EU_AUCTION"
5561
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (c) 2024 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.referral
18+
19+
import com.duckduckgo.di.scopes.AppScope
20+
import com.duckduckgo.verifiedinstallation.installsource.VerificationCheckPlayStoreInstall
21+
import com.squareup.anvil.annotations.ContributesBinding
22+
import javax.inject.Inject
23+
import timber.log.Timber
24+
25+
interface ReferrerOriginAttributeHandler {
26+
fun process(referrerParts: List<String>)
27+
}
28+
29+
@ContributesBinding(AppScope::class)
30+
class ReferrerOriginAttributeHandlerImpl @Inject constructor(
31+
private val appReferrerDataStore: AppReferrerDataStore,
32+
private val playStoreInstallChecker: VerificationCheckPlayStoreInstall,
33+
) : ReferrerOriginAttributeHandler {
34+
35+
override fun process(referrerParts: List<String>) {
36+
runCatching {
37+
Timber.v("Looking for origin attribute referrer data")
38+
var originAttributePart = extractOriginAttribute(referrerParts)
39+
40+
if (originAttributePart == null && playStoreInstallChecker.installedFromPlayStore()) {
41+
Timber.v("No origin attribute referrer data available; assigning one")
42+
originAttributePart = DEFAULT_ATTRIBUTION_FOR_PLAY_STORE_INSTALLS
43+
}
44+
45+
persistOriginAttribute(originAttributePart)
46+
}
47+
}
48+
49+
private fun extractOriginAttribute(referrerParts: List<String>): String? {
50+
val originAttributePart = referrerParts.find { it.startsWith("$ORIGIN_ATTRIBUTE_KEY=") }
51+
if (originAttributePart == null) {
52+
Timber.v("Did not find referrer origin attribute key")
53+
return null
54+
}
55+
56+
Timber.v("Found referrer origin attribute: %s", originAttributePart)
57+
58+
return originAttributePart.removePrefix("$ORIGIN_ATTRIBUTE_KEY=").also {
59+
Timber.i("Found referrer origin attribute value: %s", it)
60+
}
61+
}
62+
63+
private fun persistOriginAttribute(originAttributePart: String?) {
64+
appReferrerDataStore.utmOriginAttributeCampaign = originAttributePart
65+
}
66+
67+
companion object {
68+
const val ORIGIN_ATTRIBUTE_KEY = "origin"
69+
const val DEFAULT_ATTRIBUTION_FOR_PLAY_STORE_INSTALLS = "funnel_playstore"
70+
}
71+
}

0 commit comments

Comments
 (0)