Skip to content

Commit

Permalink
Submit autofill feedback report
Browse files Browse the repository at this point in the history
  • Loading branch information
CDRussell committed Jul 18, 2024
1 parent 581d166 commit cab53ce
Show file tree
Hide file tree
Showing 9 changed files with 423 additions and 14 deletions.
10 changes: 10 additions & 0 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ import com.duckduckgo.app.cta.ui.*
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity
import com.duckduckgo.app.fire.fireproofwebsite.data.website
import com.duckduckgo.app.global.model.PrivacyShield.PROTECTED
import com.duckduckgo.app.global.model.PrivacyShield.UNKNOWN
import com.duckduckgo.app.global.model.PrivacyShield.UNPROTECTED
import com.duckduckgo.app.global.model.orderedTrackerBlockedEntities
import com.duckduckgo.app.global.view.NonDismissibleBehavior
import com.duckduckgo.app.global.view.TextChangedWatcher
Expand Down Expand Up @@ -207,6 +209,7 @@ import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCrede
import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult.UrlOnlyMatch
import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult.UsernameMatch
import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult.UsernameMissing
import com.duckduckgo.autofill.api.PrivacyProtectionStatus
import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog
import com.duckduckgo.autofill.api.credential.saving.DuckAddressLoginCreator
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
Expand Down Expand Up @@ -2525,9 +2528,16 @@ class BrowserTabFragment :
}

private fun launchAutofillManagementScreen() {
val privacyProtectionStatus = when (viewModel.privacyShieldViewState.value?.privacyShield) {
PROTECTED -> PrivacyProtectionStatus.Enabled
UNPROTECTED -> PrivacyProtectionStatus.Disabled
else -> PrivacyProtectionStatus.Unknown
}

val screen = AutofillSettingsScreenShowSuggestionsForSiteParams(
currentUrl = webView?.url,
source = AutofillSettingsLaunchSource.BrowserOverflow,
privacyProtectionStatus = privacyProtectionStatus,
)
globalActivityStarter.start(requireContext(), screen)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@

package com.duckduckgo.autofill.api

import android.os.Parcelable
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
import java.io.Serializable
import kotlinx.parcelize.Parcelize

sealed interface AutofillScreens {

Expand All @@ -31,10 +34,12 @@ sealed interface AutofillScreens {
* Launch the Autofill management activity, which will show suggestions for the current url and the full list of available credentials
* @param currentUrl The current URL the user is viewing. This is used to show suggestions for the current site if available.
* @param source is used to indicate from where in the app Autofill management activity was launched
* @param privacyProtectionStatus status of the privacy protection for the current site
*/
data class AutofillSettingsScreenShowSuggestionsForSiteParams(
val currentUrl: String?,
val source: AutofillSettingsLaunchSource,
val privacyProtectionStatus: PrivacyProtectionStatus,
) : ActivityParams

/**
Expand All @@ -56,3 +61,24 @@ enum class AutofillSettingsLaunchSource {
InternalDevSettings,
Unknown,
}

@Parcelize
/**
* The status of the privacy protections for the current page
*/
sealed interface PrivacyProtectionStatus : Serializable, Parcelable {
@Parcelize
data object Enabled : PrivacyProtectionStatus {
private fun readResolve(): Any = Enabled
}

@Parcelize
data object Disabled : PrivacyProtectionStatus {
private fun readResolve(): Any = Disabled
}

@Parcelize
data object Unknown : PrivacyProtectionStatus {
private fun readResolve(): Any = Unknown
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PAS
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_USER_JOURNEY_SUCCESSFUL
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_USER_JOURNEY_UNSUCCESSFUL
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_USER_TOOK_NO_ACTION
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SITE_BREAKAGE_REPORT
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
Expand Down Expand Up @@ -134,6 +135,8 @@ enum class AutofillPixelNames(override val pixelName: String) : Pixel.PixelName
AUTOFILL_IMPORT_PASSWORDS_USER_JOURNEY_UNSUCCESSFUL("m_autofill_logins_import_failure"),
AUTOFILL_IMPORT_PASSWORDS_USER_JOURNEY_STARTED("m_autofill_logins_import_user_journey_started"),
AUTOFILL_IMPORT_PASSWORDS_USER_JOURNEY_RESTARTED("m_autofill_logins_import_user_journey_restarted"),

AUTOFILL_SITE_BREAKAGE_REPORT("autofill_logins_report_failure"),
}

@ContributesMultibinding(
Expand Down Expand Up @@ -163,6 +166,8 @@ object AutofillPixelsRequiringDataCleaning : PixelParamRemovalPlugin {
AUTOFILL_IMPORT_PASSWORDS_USER_JOURNEY_UNSUCCESSFUL.pixelName to PixelParameter.removeAtb(),
AUTOFILL_IMPORT_PASSWORDS_USER_JOURNEY_STARTED.pixelName to PixelParameter.removeAtb(),
AUTOFILL_IMPORT_PASSWORDS_USER_JOURNEY_RESTARTED.pixelName to PixelParameter.removeAtb(),

AUTOFILL_SITE_BREAKAGE_REPORT.pixelName to PixelParameter.removeAtb(),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* 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.reporting

import android.net.Uri
import androidx.core.net.toUri
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.autofill.api.PrivacyProtectionStatus
import com.duckduckgo.autofill.api.PrivacyProtectionStatus.Disabled
import com.duckduckgo.autofill.api.PrivacyProtectionStatus.Enabled
import com.duckduckgo.autofill.api.PrivacyProtectionStatus.Unknown
import com.duckduckgo.autofill.api.email.EmailManager
import com.duckduckgo.autofill.impl.AutofillGlobalCapabilityChecker
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SITE_BREAKAGE_REPORT
import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository
import com.duckduckgo.common.utils.DispatcherProvider
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 timber.log.Timber

interface AutofillBreakageReportSender {
fun sendBreakageReport(
url: String,
privacyProtectionStatus: PrivacyProtectionStatus,
)
}

@SingleInstanceIn(AppScope::class)
@ContributesBinding(AppScope::class)
class AutofillBreakageReportSenderImpl @Inject constructor(
private val appBuildConfig: AppBuildConfig,
private val autofillCapabilityChecker: AutofillGlobalCapabilityChecker,
private val pixel: Pixel,
private val dispatchers: DispatcherProvider,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val emailManager: EmailManager,
private val neverSavedSiteRepository: NeverSavedSiteRepository,
) : AutofillBreakageReportSender {

/**
* website: the URL + path of the website with all query parameters removed (to identify the page with an issue e.g. checkout form vs login page)
* language: the app’s language setting (e.g. `en`, `fr`) as autofill issues are frequently language specific
* autofill_enabled: whether the user has the autofill feature enabled (true / false)
* privacy_protection : whether privacy protection was enabled for the site (true / false)
* email_protection: whether a user has enabled email protection (true / false)
* never_prompt: whether a user has decided if they want to be prompted to save logins for this site (true / false)
*/

override fun sendBreakageReport(
url: String,
privacyProtectionStatus: PrivacyProtectionStatus,
) {
appCoroutineScope.launch(dispatchers.io()) {
val params = mapOf(
"website" to formatUrl(url),
"language" to formatLanguage(),
"autofill_enabled" to formatAutofillEnabledStatus(),
"privacy_protection" to formatPrivacyProtectionStatus(privacyProtectionStatus),
"email_protection" to formatEmailProtectionStatus(),
"never_prompt" to formatNeverProtectThisSiteStatus(url),
)
Timber.d("Sending autofill breakage report %s", params)

pixel.fire(AUTOFILL_SITE_BREAKAGE_REPORT, parameters = params)
}
}

private suspend fun formatNeverProtectThisSiteStatus(url: String): String {
return neverSavedSiteRepository.isInNeverSaveList(url).toString()
}

private fun formatEmailProtectionStatus(): String {
return emailManager.isSignedIn().toString()
}

private fun formatPrivacyProtectionStatus(privacyProtectionStatus: PrivacyProtectionStatus): String {
return when (privacyProtectionStatus) {
Enabled -> true.toString()
Disabled -> false.toString()
Unknown -> "unknown"
}
}

private suspend fun formatAutofillEnabledStatus(): String {
return autofillCapabilityChecker.isAutofillEnabledByUser().toString()
}

private fun formatUrl(url: String): String {
val uri = url.toUri()
return Uri.Builder()
.scheme(uri.scheme)
.authority(uri.authority)
.path(uri.path)
.fragment(uri.fragment)
.build()
.toString()
}

private fun formatLanguage(): String {
return appBuildConfig.deviceLocale.language
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.duckduckgo.autofill.api.AutofillScreens.AutofillSettingsScreen
import com.duckduckgo.autofill.api.AutofillScreens.AutofillSettingsScreenDirectlyViewCredentialsParams
import com.duckduckgo.autofill.api.AutofillScreens.AutofillSettingsScreenShowSuggestionsForSiteParams
import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource
import com.duckduckgo.autofill.api.PrivacyProtectionStatus
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.autofill.impl.R
import com.duckduckgo.autofill.impl.databinding.ActivityAutofillSettingsBinding
Expand Down Expand Up @@ -248,10 +249,12 @@ class AutofillManagementActivity : DuckDuckGoActivity() {
private fun showListMode() {
resetToolbar()
val currentUrl = extractSuggestionsUrl()
val privacyProtectionStatus = extractPrivacyProtectionStatus()
Timber.v("showListMode. currentUrl is %s", currentUrl)

supportFragmentManager.commitNow {
replace(R.id.fragment_container_view, AutofillManagementListMode.instance(currentUrl), TAG_ALL_CREDENTIALS)
val fragment = AutofillManagementListMode.instance(currentUrl, privacyProtectionStatus)
replace(R.id.fragment_container_view, fragment, TAG_ALL_CREDENTIALS)
}
}

Expand Down Expand Up @@ -391,6 +394,12 @@ class AutofillManagementActivity : DuckDuckGoActivity() {
return null
}

private fun extractPrivacyProtectionStatus(): PrivacyProtectionStatus {
intent.getActivityParams(AutofillSettingsScreenShowSuggestionsForSiteParams::class.java)?.let {
return it.privacyProtectionStatus
} ?: return PrivacyProtectionStatus.Unknown
}

companion object {
private const val TAG_LOCKED = "tag_fragment_locked"
private const val TAG_DISABLED = "tag_fragment_disabled"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource
import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.BrowserOverflow
import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.SettingsActivity
import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.Sync
import com.duckduckgo.autofill.api.PrivacyProtectionStatus
import com.duckduckgo.autofill.api.PrivacyProtectionStatus.Unknown
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.autofill.api.email.EmailManager
import com.duckduckgo.autofill.impl.R
Expand All @@ -40,6 +42,7 @@ import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_MANUALLY_S
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_NEVER_SAVE_FOR_THIS_SITE_CONFIRMATION_PROMPT_CONFIRMED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_NEVER_SAVE_FOR_THIS_SITE_CONFIRMATION_PROMPT_DISMISSED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_NEVER_SAVE_FOR_THIS_SITE_CONFIRMATION_PROMPT_DISPLAYED
import com.duckduckgo.autofill.impl.reporting.AutofillBreakageReportSender
import com.duckduckgo.autofill.impl.reporting.remoteconfig.AutofillSiteBreakageReportingFeature
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository
Expand Down Expand Up @@ -124,6 +127,7 @@ class AutofillSettingsViewModel @Inject constructor(
private val reportBreakageFeature: AutofillSiteBreakageReportingFeature,
private val reportBreakageFeatureExceptions: AutofillSiteBreakageReportingFeatureRepository,
private val urlMatcher: AutofillUrlMatcher,
private val autofillBreakageReportSender: AutofillBreakageReportSender,
) : ViewModel() {

private val _viewState = MutableStateFlow(ViewState())
Expand Down Expand Up @@ -152,6 +156,7 @@ class AutofillSettingsViewModel @Inject constructor(
private var combineJob: Job? = null

private var currentUrl: String? = null
private var privacyProtectionStatus: PrivacyProtectionStatus = Unknown

fun onCopyUsername(username: String?) {
username?.let { clipboardInteractor.copyToClipboard(it, isSensitive = false) }
Expand Down Expand Up @@ -715,12 +720,10 @@ class AutofillSettingsViewModel @Inject constructor(
}
}

fun updateCurrentUrl(currentUrl: String?) {
this.currentUrl = currentUrl
}

fun userConfirmedSendBreakageReport(eTldPlusOne: String) {
// todo send the pixel
fun userConfirmedSendBreakageReport() {
currentUrl?.let {
autofillBreakageReportSender.sendBreakageReport(it, privacyProtectionStatus)
}

// todo record feedback sent timestamp for this domain. todo - work out where to record this

Expand All @@ -729,6 +732,11 @@ class AutofillSettingsViewModel @Inject constructor(
addCommand(ListModeCommand.ShowUserReportSentMessage)
}

fun updateCurrentSite(currentUrl: String?, privacyProtectionStatus: PrivacyProtectionStatus) {
this.currentUrl = currentUrl
this.privacyProtectionStatus = privacyProtectionStatus
}

data class ViewState(
val autofillEnabled: Boolean = true,
val showAutofillEnabledToggle: Boolean = true,
Expand Down
Loading

0 comments on commit cab53ce

Please sign in to comment.