Skip to content

Commit

Permalink
Detect System AutofillService usage within WebView (#3643)
Browse files Browse the repository at this point in the history
<!--
Note: This checklist is a reminder of our shared engineering
expectations.
The items in Bold are required
If your PR involves UI changes:
1. Upload screenshots or screencasts that illustrate the changes before
/ after
2. Add them under the UI changes section (feel free to add more columns
if needed)
If your PR does not involve UI changes, you can remove the **UI
changes** section

At a minimum, make sure your changes are tested in API 23 and one of the
more recent API levels available.
-->

Task/Issue URL:
https://app.asana.com/0/488551667048375/1205669280706362/f

### Description
Detect system-level `AutofillService` autofill event to gain insights
into usage.

### Steps to test this PR

- [ ] Ensure you have a system-level Autofill provider set up (search
for `Autofill` in system settings)
- [ ] Visit https://fill.dev/form/registration-email and tap on the
email field
- [ ] Choose to autofill using the system-level provider, which will
either be an inline suggestion on the form field or a button at the top
of the keyboard
- [ ] Verify a pixel is sent
- [ ] Repeat, and verify the pixel is not sent (it's limited to once per
day)
- [ ] Set system clock one day forward and repeat; verify pixel is sent
  • Loading branch information
CDRussell authored and joshliebe committed Nov 7, 2023
1 parent 8d0fbaa commit dae0234
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ import com.duckduckgo.autofill.api.credential.saving.DuckAddressLoginCreator
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.autofill.api.domain.app.LoginTriggerType
import com.duckduckgo.autofill.api.store.AutofillStore.ContainsCredentialsResult.*
import com.duckduckgo.autofill.api.systemautofill.SystemAutofillUsageMonitor
import com.duckduckgo.browser.api.brokensite.BrokenSiteData
import com.duckduckgo.contentscopescripts.api.ContentScopeScripts
import com.duckduckgo.di.scopes.FragmentScope
Expand Down Expand Up @@ -236,7 +237,8 @@ class BrowserTabFragment :
TrackersAnimatorListener,
DownloadConfirmationDialogListener,
SitePermissionsGrantedListener,
AutofillEventListener {
AutofillEventListener,
SystemAutofillListener {

private val supervisorJob = SupervisorJob()

Expand Down Expand Up @@ -390,6 +392,9 @@ class BrowserTabFragment :
@Inject
lateinit var contentScopeScripts: ContentScopeScripts

@Inject
lateinit var systemAutofillUsageMonitor: SystemAutofillUsageMonitor

private var urlExtractingWebView: UrlExtractingWebView? = null

var messageFromPreviousTab: Message? = null
Expand Down Expand Up @@ -2062,6 +2067,8 @@ class BrowserTabFragment :
}
}
}

it.systemAutofillListener = this
}

private fun injectAutofillCredentials(
Expand Down Expand Up @@ -3566,4 +3573,8 @@ class BrowserTabFragment :
val roomParameters = "?skipMediaPermissionPrompt"
webView?.loadUrl("${webView?.url.orEmpty()}$roomParameters")
}

override fun systemAutofillPerformed() {
systemAutofillUsageMonitor.onSystemAutofillUsed()
}
}
16 changes: 16 additions & 0 deletions app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoWebView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ package com.duckduckgo.app.browser
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.util.SparseArray
import android.view.MotionEvent
import android.view.autofill.AutofillValue
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.webkit.WebView
import androidx.core.util.isNotEmpty
import androidx.core.view.NestedScrollingChild
import androidx.core.view.NestedScrollingChildHelper
import androidx.core.view.ViewCompat
Expand All @@ -48,6 +51,8 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild {
private var nestedOffsetY: Int = 0
private var nestedScrollHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)

var systemAutofillListener: SystemAutofillListener? = null

constructor(context: Context) : this(context, null)
constructor(
context: Context,
Expand Down Expand Up @@ -212,6 +217,13 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild {
}
}

override fun autofill(values: SparseArray<AutofillValue>) {
super.autofill(values)
if (values.isNotEmpty()) {
systemAutofillListener?.systemAutofillPerformed()
}
}

companion object {

/*
Expand All @@ -221,3 +233,7 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild {
private const val IME_FLAG_NO_PERSONALIZED_LEARNING = 0x1000000
}
}

interface SystemAutofillListener {
fun systemAutofillPerformed()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2023 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.autofill.api.systemautofill

/**
* Interface for monitoring system autofill usage
*/
interface SystemAutofillUsageMonitor {

/**
* Used to notify that the user has used system AutofillService to fill in any type of data into a form in the WebView
*/
fun onSystemAutofillUsed()
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ enum class AutofillPixelNames(override val pixelName: String) : Pixel.PixelName
EMAIL_USE_ALIAS("email_filled_random"),
EMAIL_USE_ADDRESS("email_filled_main"),
EMAIL_TOOLTIP_DISMISSED("email_tooltip_dismissed"),

SYSTEM_AUTOFILL_USED("m_autofill_system_autofillservice_autofilled"),
}

@ContributesMultibinding(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2023 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.autofill.impl.systemautofill

import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.global.DispatcherProvider
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.autofill.api.systemautofill.SystemAutofillUsageMonitor
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.threeten.bp.Instant
import org.threeten.bp.ZoneOffset
import org.threeten.bp.format.DateTimeFormatter

@SingleInstanceIn(AppScope::class)
@ContributesBinding(AppScope::class)
class RealSystemAutofillUsageMonitor @Inject constructor(
private val pixel: Pixel,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val dispatchers: DispatcherProvider,
private val context: Context,
) : SystemAutofillUsageMonitor {

private val preferences: SharedPreferences by lazy {
context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE)
}

override fun onSystemAutofillUsed() {
appCoroutineScope.launch(dispatchers.io()) {
tryToFireDailyPixel(AutofillPixelNames.SYSTEM_AUTOFILL_USED.pixelName)
}
}

private fun tryToFireDailyPixel(pixelName: String) {
val now = getUtcIsoLocalDate()
val timestamp = preferences.getString(pixelName.appendTimestampSuffix(), null)

if (canFirePixelToday(timestamp, now)) {
pixel.fire(pixelName).also { preferences.edit { putString(pixelName.appendTimestampSuffix(), now) } }
}
}

private fun canFirePixelToday(
timestamp: String?,
now: String,
): Boolean {
return timestamp == null || now > timestamp
}

private fun String.appendTimestampSuffix(): String = "${this}_timestamp"

/**
* returns today's date in YYYY-MM-dd format
*/
private fun getUtcIsoLocalDate(): String {
return Instant.now().atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_LOCAL_DATE)
}

companion object {
private const val FILENAME = "com.duckduckgo.autofill.impl.systemautofill.RealSystemAutofillUsageMonitor"
}
}

0 comments on commit dae0234

Please sign in to comment.