Skip to content

Commit

Permalink
Autofill engagement pixels
Browse files Browse the repository at this point in the history
  • Loading branch information
CDRussell committed May 15, 2024
1 parent db5a789 commit a20143a
Show file tree
Hide file tree
Showing 23 changed files with 686 additions and 6 deletions.
1 change: 1 addition & 0 deletions autofill/autofill-impl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ dependencies {
implementation AndroidX.work.runtimeKtx
implementation AndroidX.room.ktx
implementation AndroidX.biometric
implementation AndroidX.dataStore.preferences

implementation "net.zetetic:android-database-sqlcipher:_"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2024 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

import com.duckduckgo.anvil.annotations.ContributesPluginPoint
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.di.scopes.AppScope

@ContributesPluginPoint(AppScope::class)
interface PasswordStoreEventListener {
fun onCredentialAdded(loginCredentials: LoginCredentials) {}
fun onCredentialUpdated(loginCredentials: LoginCredentials) {}
fun onCredentialsDeleted(loginCredentials: LoginCredentials) {}
fun onCredentialsDeleted(loginCredentialsList: List<LoginCredentials>) {}
fun onCredentialReinserted(loginCredentials: LoginCredentials) {}
fun onCredentialReinserted(loginCredentialsList: List<LoginCredentials>) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import com.duckduckgo.autofill.store.LastUpdatedTimeProvider
import com.duckduckgo.autofill.sync.SyncCredentialsListener
import com.duckduckgo.common.utils.DefaultDispatcherProvider
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
Expand All @@ -51,8 +52,11 @@ class SecureStoreBackedAutofillStore @Inject constructor(
private val dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider(),
private val autofillUrlMatcher: AutofillUrlMatcher,
private val syncCredentialsListener: SyncCredentialsListener,
passwordStoreEventListenersPlugins: PluginPoint<PasswordStoreEventListener>,
) : InternalAutofillStore {

private val passwordStoreEventListeners = passwordStoreEventListenersPlugins.getPlugins()

override val autofillAvailable: Boolean
get() = secureStorage.canAccessSecureStorage()

Expand Down Expand Up @@ -130,6 +134,7 @@ class SecureStoreBackedAutofillStore @Inject constructor(
return withContext(dispatcherProvider.io()) {
secureStorage.addWebsiteLoginDetailsWithCredentials(webSiteLoginCredentials)?.toLoginCredentials().also {
syncCredentialsListener.onCredentialAdded(it?.id!!)
passwordStoreEventListeners.forEach { listener -> listener.onCredentialAdded(it) }
}
}
}
Expand Down Expand Up @@ -162,6 +167,7 @@ class SecureStoreBackedAutofillStore @Inject constructor(
val modified = it.copy(password = credentials.password, details = modifiedDetails)
updatedCredentials = secureStorage.updateWebsiteLoginDetailsWithCredentials(modified)?.also {
syncCredentialsListener.onCredentialUpdated(it.details.id!!)
passwordStoreEventListeners.forEach { listener -> listener.onCredentialUpdated(it.toLoginCredentials()) }
}
}

Expand All @@ -187,10 +193,13 @@ class SecureStoreBackedAutofillStore @Inject constructor(
}

override suspend fun deleteCredentials(id: Long): LoginCredentials? {
val existingCredential = secureStorage.getWebsiteLoginDetailsWithCredentials(id)
val existingCredential = secureStorage.getWebsiteLoginDetailsWithCredentials(id) ?: return null
secureStorage.deleteWebsiteLoginDetailsWithCredentials(id)
syncCredentialsListener.onCredentialRemoved(id)
return existingCredential?.toLoginCredentials()

return existingCredential.toLoginCredentials().also {
syncCredentialsListener.onCredentialRemoved(id)
passwordStoreEventListeners.forEach { listener -> listener.onCredentialsDeleted(it) }
}
}

override suspend fun deleteAllCredentials(): List<LoginCredentials> {
Expand All @@ -199,7 +208,9 @@ class SecureStoreBackedAutofillStore @Inject constructor(
secureStorage.deleteWebSiteLoginDetailsWithCredentials(idsToDelete)
Timber.i("Deleted %d credentials", idsToDelete.size)
syncCredentialsListener.onCredentialRemoved(idsToDelete)
return savedCredentials.map { it.toLoginCredentials() }
return savedCredentials.map { it.toLoginCredentials() }.also {
passwordStoreEventListeners.forEach { listener -> listener.onCredentialsDeleted(it) }
}
}

override suspend fun updateCredentials(credentials: LoginCredentials, refreshLastUpdatedTimestamp: Boolean): LoginCredentials? {
Expand All @@ -213,6 +224,7 @@ class SecureStoreBackedAutofillStore @Inject constructor(
credentials.copy(lastUpdatedMillis = lastUpdated, domain = cleanedDomain).toWebsiteLoginCredentials(),
)?.toLoginCredentials()?.also {
syncCredentialsListener.onCredentialUpdated(it.id!!)
passwordStoreEventListeners.forEach { listener -> listener.onCredentialUpdated(it) }
}
}

Expand Down Expand Up @@ -277,6 +289,7 @@ class SecureStoreBackedAutofillStore @Inject constructor(
withContext(dispatcherProvider.io()) {
secureStorage.addWebsiteLoginDetailsWithCredentials(credentials.prepareForReinsertion())?.also {
syncCredentialsListener.onCredentialAdded(it.details.id!!)
passwordStoreEventListeners.forEach { listener -> listener.onCredentialReinserted(it.toLoginCredentials()) }
}
}
}
Expand All @@ -287,6 +300,7 @@ class SecureStoreBackedAutofillStore @Inject constructor(
secureStorage.addWebsiteLoginDetailsWithCredentials(mappedCredentials).also {
val ids = mappedCredentials.mapNotNull { it.details.id }
syncCredentialsListener.onCredentialsAdded(ids)
passwordStoreEventListeners.forEach { listener -> listener.onCredentialReinserted(mappedCredentials.map { it.toLoginCredentials() }) }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.duckduckgo.autofill.store.LastUpdatedTimeProvider
import com.duckduckgo.autofill.store.RealAutofillPrefsStore
import com.duckduckgo.autofill.store.RealInternalTestUserStore
import com.duckduckgo.autofill.store.RealLastUpdatedTimeProvider
import com.duckduckgo.autofill.store.engagement.AutofillEngagementDatabase
import com.duckduckgo.autofill.store.feature.AutofillDefaultStateDecider
import com.duckduckgo.autofill.store.feature.AutofillFeatureRepository
import com.duckduckgo.autofill.store.feature.RealAutofillDefaultStateDecider
Expand Down Expand Up @@ -136,6 +137,16 @@ class AutofillModule {
): CredentialsSyncMetadataDao {
return database.credentialsSyncDao()
}

@Provides
@SingleInstanceIn(AppScope::class)
fun providesAutofillEngagementDb(
context: Context,
): AutofillEngagementDatabase {
return Room.databaseBuilder(context, AutofillEngagementDatabase::class.java, "autofill_engagement.db")
.addMigrations(*AutofillEngagementDatabase.ALL_MIGRATIONS)
.build()
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin
import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog
import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.UseEmailResultType.*
import com.duckduckgo.autofill.api.email.EmailManager
import com.duckduckgo.autofill.impl.engagement.DataAutofilledListener
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_TOOLTIP_DISMISSED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ADDRESS
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ALIAS
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject
Expand All @@ -52,6 +54,7 @@ class ResultHandlerEmailProtectionChooseEmail @Inject constructor(
private val dispatchers: DispatcherProvider,
private val pixel: Pixel,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val autofilledListeners: PluginPoint<DataAutofilledListener>,
) : AutofillFragmentResultsPlugin {

override fun processResult(
Expand All @@ -68,8 +71,14 @@ class ResultHandlerEmailProtectionChooseEmail @Inject constructor(
val originalUrl = result.getString(EmailProtectionChooseEmailDialog.KEY_URL) ?: return

when (userSelection) {
UsePersonalEmailAddress -> onSelectedToUsePersonalAddress(originalUrl, autofillCallback)
UsePrivateAliasAddress -> onSelectedToUsePrivateAlias(originalUrl, autofillCallback)
UsePersonalEmailAddress -> {
onSelectedToUsePersonalAddress(originalUrl, autofillCallback)
notifyAutofillListenersDuckAddressFilled()
}
UsePrivateAliasAddress -> {
onSelectedToUsePrivateAlias(originalUrl, autofillCallback)
notifyAutofillListenersDuckAddressFilled()
}
DoNotUseEmailProtection -> onSelectedNotToUseEmailProtection()
}
}
Expand Down Expand Up @@ -133,4 +142,10 @@ class ResultHandlerEmailProtectionChooseEmail @Inject constructor(
override fun resultKey(tabId: String): String {
return EmailProtectionChooseEmailDialog.resultKey(tabId)
}

private fun notifyAutofillListenersDuckAddressFilled() {
autofilledListeners.getPlugins().forEach {
it.onAutofilledDuckAddress()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2024 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.engagement

import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.statistics.api.RefreshRetentionAtbPlugin
import com.duckduckgo.autofill.impl.engagement.store.AutofillEngagementRepository
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

@ContributesMultibinding(AppScope::class)
class AutofillRefreshRetentionListener @Inject constructor(
private val engagementRepository: AutofillEngagementRepository,
@AppCoroutineScope private val coroutineScope: CoroutineScope,
private val dispatchers: DispatcherProvider,
) : RefreshRetentionAtbPlugin {
override fun onSearchRetentionAtbRefreshed() {
coroutineScope.launch(dispatchers.io()) {
engagementRepository.recordSearchedToday()
}
}

override fun onAppRetentionAtbRefreshed() {
// no-op
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2024 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.engagement

import com.duckduckgo.anvil.annotations.ContributesPluginPoint
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.autofill.impl.engagement.store.AutofillEngagementRepository
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber

@ContributesPluginPoint(AppScope::class)
interface DataAutofilledListener {
fun onAutofilledSavedPassword()
fun onAutofilledDuckAddress()
fun onUsedGeneratedPassword()
}

@ContributesMultibinding(AppScope::class)
class DefaultDataAutofilledListener @Inject constructor(
private val engagementRepository: AutofillEngagementRepository,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val dispatchers: DispatcherProvider,
) : DataAutofilledListener {

override fun onAutofilledSavedPassword() {
Timber.v("onAutofilledSavedPassword, recording autofilled today")
recordAutofilledToday()
}

override fun onAutofilledDuckAddress() {
Timber.v("onAutofilledDuckAddress, recording autofilled today")
recordAutofilledToday()
}

override fun onUsedGeneratedPassword() {
Timber.v("onUsedGeneratedPassword, recording autofilled today")
recordAutofilledToday()
}

private fun recordAutofilledToday() {
appCoroutineScope.launch(dispatchers.io()) {
engagementRepository.recordAutofilledToday()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2024 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.engagement

import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.autofill.impl.PasswordStoreEventListener
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames
import com.duckduckgo.browser.api.UserBrowserProperties
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber

@ContributesMultibinding(AppScope::class)
class EngagementPasswordAddedListener @Inject constructor(
private val userBrowserProperties: UserBrowserProperties,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val dispatchers: DispatcherProvider,
private val pixel: Pixel,
) : PasswordStoreEventListener {

override fun onCredentialAdded(loginCredentials: LoginCredentials) {
appCoroutineScope.launch(dispatchers.io()) {
val daysInstalled = userBrowserProperties.daysSinceInstalled()
Timber.i("onCredentialAdded daysInstalled: $daysInstalled")
if (daysInstalled < 7) {
Timber.v("onCredentialAdded within first week")
pixel.fire(AutofillPixelNames.AUTOFILL_ENGAGEMENT_ONBOARDED_USER, type = DAILY)
}
}
}
}
Loading

0 comments on commit a20143a

Please sign in to comment.