Skip to content

Commit 7dc623c

Browse files
authored
Ensure observability of system autofill event (#6800)
Task/Issue URL: https://app.asana.com/1/137249556945/project/608920331025315/task/1211357591806022?focus=true ### Description Adds pixel to system autofill events. ### Steps to test this PR Logcat filter: `autofill_systemautofill_used` - [x] Make sure you have a System Autofill provider set up (e.g., Google Passwords, 1Password, Bitwarden etc...) - [x] Add a saved login credential to the system autofill provider for `fill.dev` - [x] Visit https://fill.dev/form/login-simple and tap on the username or password fields. Accept the system autofill provider's suggestion - [x] Verify `autofill_systemautofill_used` - [x] Repeat on another field (refresh the page if required) and verify `autofill_systemautofill_used` is ignored (pixel type is `daily`) #### With feature flag disabled - [x] Fresh install - [x] Use feature flag inventory to disable `canDetectSystemAutofillEngagement` - [x] Follow test steps from before and verify you don't see `autofill_systemautofill_used` in the logcat at all Co-authored-by: Craig Russell <[email protected]>
1 parent e697ea6 commit 7dc623c

File tree

6 files changed

+164
-0
lines changed

6 files changed

+164
-0
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ import com.duckduckgo.app.browser.applinks.AppLinksLauncher
113113
import com.duckduckgo.app.browser.applinks.AppLinksSnackBarConfigurator
114114
import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter
115115
import com.duckduckgo.app.browser.autocomplete.SuggestionItemDecoration
116+
import com.duckduckgo.app.browser.autofill.SystemAutofillEngagement
116117
import com.duckduckgo.app.browser.commands.Command
117118
import com.duckduckgo.app.browser.commands.Command.OpenBrokenSiteLearnMore
118119
import com.duckduckgo.app.browser.commands.Command.ReportBrokenSiteError
@@ -617,6 +618,9 @@ class BrowserTabFragment :
617618
@Inject
618619
lateinit var autofillFragmentResultListeners: PluginPoint<AutofillFragmentResultsPlugin>
619620

621+
@Inject
622+
lateinit var systemAutofillEngagement: SystemAutofillEngagement
623+
620624
private var isActiveTab: Boolean = false
621625

622626
private val downloadMessagesJob = ConflatedJob()
@@ -3346,6 +3350,10 @@ class BrowserTabFragment :
33463350
}
33473351

33483352
private fun configureWebViewForAutofill(it: DuckDuckGoWebView) {
3353+
it.setSystemAutofillCallback {
3354+
systemAutofillEngagement.onSystemAutofillEvent()
3355+
}
3356+
33493357
browserAutofill.addJsInterface(it, autofillCallback, this, null, tabId)
33503358

33513359
autofillFragmentResultListeners.getPlugins().forEach { plugin ->
@@ -3907,6 +3915,7 @@ class BrowserTabFragment :
39073915
private fun destroyWebView() {
39083916
if (::webViewContainer.isInitialized) webViewContainer.removeAllViews()
39093917
webView?.let {
3918+
it.removeSystemAutofillCallback()
39103919
webViewClient.destroy(it)
39113920
it.destroy()
39123921
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import android.content.Context
2121
import android.os.Message
2222
import android.print.PrintDocumentAdapter
2323
import android.util.AttributeSet
24+
import android.util.SparseArray
2425
import android.view.MotionEvent
26+
import android.view.autofill.AutofillValue
2527
import android.view.inputmethod.EditorInfo
2628
import android.view.inputmethod.InputConnection
2729
import android.webkit.DownloadListener
@@ -61,6 +63,7 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
6163
private var enableSwipeRefreshCallback: ((Boolean) -> Unit)? = null
6264
private var hasGestureFinished = true
6365
private var canSwipeToRefresh = true
66+
private var systemAutofillCallback: (() -> Unit)? = null
6467

6568
private var lastY: Int = 0
6669
private var lastDeltaY: Int = 0
@@ -194,6 +197,20 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
194197
}
195198
}
196199

200+
override fun autofill(value: AutofillValue?) {
201+
if (!isDestroyed) {
202+
super.autofill(value)
203+
systemAutofillCallback?.invoke()
204+
}
205+
}
206+
207+
override fun autofill(values: SparseArray<AutofillValue?>) {
208+
if (!isDestroyed) {
209+
super.autofill(values)
210+
systemAutofillCallback?.invoke()
211+
}
212+
}
213+
197214
override fun getUrl(): String? {
198215
if (isDestroyed) return null
199216
return super.getUrl()
@@ -411,6 +428,14 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 {
411428
enableSwipeRefreshCallback = null
412429
}
413430

431+
fun setSystemAutofillCallback(callback: () -> Unit) {
432+
systemAutofillCallback = callback
433+
}
434+
435+
fun removeSystemAutofillCallback() {
436+
systemAutofillCallback = null
437+
}
438+
414439
private fun enableSwipeRefresh(enable: Boolean) {
415440
enableSwipeRefreshCallback?.invoke(enable && contentAllowsSwipeToRefresh)
416441
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (c) 2025 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.browser.autofill
18+
19+
import com.duckduckgo.app.di.AppCoroutineScope
20+
import com.duckduckgo.app.statistics.pixels.Pixel
21+
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily
22+
import com.duckduckgo.autofill.api.AutofillFeature
23+
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames
24+
import com.duckduckgo.common.utils.DispatcherProvider
25+
import com.duckduckgo.di.scopes.AppScope
26+
import com.squareup.anvil.annotations.ContributesBinding
27+
import javax.inject.Inject
28+
import kotlinx.coroutines.CoroutineScope
29+
import kotlinx.coroutines.launch
30+
import logcat.LogPriority.VERBOSE
31+
import logcat.logcat
32+
33+
interface SystemAutofillEngagement {
34+
fun onSystemAutofillEvent()
35+
}
36+
37+
@ContributesBinding(AppScope::class)
38+
class RealSystemAutofillEngagement @Inject constructor(
39+
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
40+
private val dispatchers: DispatcherProvider,
41+
private val autofillFeature: AutofillFeature,
42+
private val pixel: Pixel,
43+
) : SystemAutofillEngagement {
44+
45+
override fun onSystemAutofillEvent() {
46+
logcat(VERBOSE) { "System autofill event received" }
47+
appCoroutineScope.launch(dispatchers.io()) {
48+
if (autofillFeature.canDetectSystemAutofillEngagement().isEnabled()) {
49+
pixel.fire(AutofillPixelNames.AUTOFILL_SYSTEM_AUTOFILL_USED, type = Daily())
50+
}
51+
}
52+
}
53+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (c) 2025 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.browser.autofill
18+
19+
import android.annotation.SuppressLint
20+
import com.duckduckgo.app.statistics.pixels.Pixel
21+
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily
22+
import com.duckduckgo.autofill.api.AutofillFeature
23+
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SYSTEM_AUTOFILL_USED
24+
import com.duckduckgo.common.test.CoroutineTestRule
25+
import com.duckduckgo.common.utils.DispatcherProvider
26+
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
27+
import com.duckduckgo.feature.toggles.api.Toggle.State
28+
import kotlinx.coroutines.test.runTest
29+
import org.junit.Before
30+
import org.junit.Rule
31+
import org.junit.Test
32+
import org.mockito.kotlin.mock
33+
import org.mockito.kotlin.never
34+
import org.mockito.kotlin.verify
35+
36+
@SuppressLint("DenyListedApi")
37+
class RealSystemAutofillEngagementTest {
38+
39+
@get:Rule
40+
val coroutineTestRule = CoroutineTestRule()
41+
private val pixel: Pixel = mock()
42+
private val dispatchers: DispatcherProvider = coroutineTestRule.testDispatcherProvider
43+
private val feature = FakeFeatureToggleFactory.create(AutofillFeature::class.java)
44+
45+
private lateinit var testee: RealSystemAutofillEngagement
46+
47+
@Before
48+
fun setup() {
49+
testee = RealSystemAutofillEngagement(
50+
appCoroutineScope = coroutineTestRule.testScope,
51+
dispatchers = dispatchers,
52+
autofillFeature = feature,
53+
pixel = pixel,
54+
)
55+
}
56+
57+
@Test
58+
fun whenFeatureEnabledThenPixelFired() = runTest {
59+
feature.canDetectSystemAutofillEngagement().setRawStoredState(State(true))
60+
testee.onSystemAutofillEvent()
61+
verify(pixel).fire(AUTOFILL_SYSTEM_AUTOFILL_USED, type = Daily())
62+
}
63+
64+
@Test
65+
fun whenFeatureDisabledThenPixelNotFired() = runTest {
66+
feature.canDetectSystemAutofillEngagement().setRawStoredState(State(false))
67+
testee.onSystemAutofillEvent()
68+
verify(pixel, never()).fire(AUTOFILL_SYSTEM_AUTOFILL_USED, type = Daily())
69+
}
70+
}

autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillFeature.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,7 @@ interface AutofillFeature {
160160

161161
@Toggle.DefaultValue(defaultValue = DefaultFeatureValue.TRUE)
162162
fun canReAuthenticateGoogleLoginsAutomatically(): Toggle
163+
164+
@Toggle.DefaultValue(defaultValue = DefaultFeatureValue.TRUE)
165+
fun canDetectSystemAutofillEngagement(): Toggle
163166
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelNames.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SITE_BREAK
6262
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SITE_BREAKAGE_REPORT_CONFIRMATION_DISPLAYED
6363
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SYNC_DESKTOP_PASSWORDS_CTA_BUTTON
6464
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SYNC_DESKTOP_PASSWORDS_OVERFLOW_MENU
65+
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SYSTEM_AUTOFILL_USED
6566
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_TOOLTIP_DISMISSED
6667
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ADDRESS
6768
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ALIAS
@@ -206,6 +207,8 @@ enum class AutofillPixelNames(override val pixelName: String) : Pixel.PixelName
206207

207208
AUTOFILL_SETTINGS_OPENED("autofill_settings_opened"),
208209
AUTOFILL_NEVER_SAVE_FOR_THIS_SITE_OVERFLOW_MENU("autofill_reset_excluded_overflow_menu_tapped"),
210+
211+
AUTOFILL_SYSTEM_AUTOFILL_USED("autofill_systemautofill_used"),
209212
}
210213

211214
object AutofillPixelParameters {
@@ -281,6 +284,7 @@ object AutofillPixelsRequiringDataCleaning : PixelParamRemovalPlugin {
281284
AUTOFILL_NEVER_SAVE_FOR_THIS_SITE_OVERFLOW_MENU.pixelName to PixelParameter.removeAtb(),
282285

283286
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_MAIN_APP_SETTINGS_HIDDEN.pixelName to PixelParameter.removeAtb(),
287+
AUTOFILL_SYSTEM_AUTOFILL_USED.pixelName to PixelParameter.removeAtb(),
284288
)
285289
}
286290
}

0 commit comments

Comments
 (0)