Skip to content

Commit

Permalink
UI for the showing/hiding/dismissing/accessing the survey
Browse files Browse the repository at this point in the history
  • Loading branch information
CDRussell committed May 7, 2024
1 parent 65177a1 commit c4dfe03
Show file tree
Hide file tree
Showing 16 changed files with 698 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ enum class AutofillPixelNames(override val pixelName: String) : Pixel.PixelName
"m_autofill_device_capability_secure_storage_unavailable_and_device_auth_disabled",
),
AUTOFILL_DEVICE_CAPABILITY_UNKNOWN_ERROR("m_autofill_device_capability_unknown"),

AUTOFILL_SURVEY_AVAILABLE_PROMPT_DISPLAYED("m_autofill_management_screen_visit_survey_available"),
}

@ContributesMultibinding(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
package com.duckduckgo.autofill.impl.ui.credential.management

import android.os.Bundle
import android.system.Os.remove
import android.text.TextUtils.replace
import android.view.WindowManager
import androidx.core.view.isVisible
import androidx.fragment.app.commit
Expand Down Expand Up @@ -133,8 +131,8 @@ class AutofillManagementActivity : DuckDuckGoActivity() {

private fun setupInitialState() {
when (val mode = extractViewMode()) {
is ViewMode.ListMode -> viewModel.onShowListMode()
is ViewMode.ListModeWithSuggestions -> viewModel.onShowListMode()
is ViewMode.ListMode -> viewModel.onInitialiseListMode()
is ViewMode.ListModeWithSuggestions -> viewModel.onInitialiseListMode()
is ViewMode.CredentialMode -> viewModel.onViewCredentials(mode.loginCredentials)
}
}
Expand Down Expand Up @@ -252,7 +250,7 @@ class AutofillManagementActivity : DuckDuckGoActivity() {
if (credentialModeLaunchedDirectly()) {
finish()
} else {
viewModel.onShowListMode()
viewModel.onReturnToListModeFromCredentialMode()
}
}

Expand Down Expand Up @@ -339,7 +337,7 @@ class AutofillManagementActivity : DuckDuckGoActivity() {
if (credentialModeLaunchedDirectly()) {
finish()
} else {
viewModel.onShowListMode()
viewModel.onReturnToListModeFromCredentialMode()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.duckduckgo.autofill.impl.InternalAutofillCapabilityChecker
import com.duckduckgo.autofill.impl.R
import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator
import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator.AuthConfiguration
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_DISABLED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_ENABLED
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_NEVER_SAVE_FOR_THIS_SITE_CONFIRMATION_PROMPT_CONFIRMED
Expand Down Expand Up @@ -72,6 +73,8 @@ import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsVie
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.PromptUserToAuthenticateMassDeletion
import com.duckduckgo.autofill.impl.ui.credential.management.neversaved.NeverSavedSitesViewState
import com.duckduckgo.autofill.impl.ui.credential.management.searching.CredentialListFilter
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurvey
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurvey.SurveyDetails
import com.duckduckgo.autofill.impl.ui.credential.management.viewing.duckaddress.DuckAddressIdentifier
import com.duckduckgo.autofill.impl.ui.credential.repository.DuckAddressStatusRepository
import com.duckduckgo.autofill.impl.ui.credential.repository.DuckAddressStatusRepository.ActivationStatusResult
Expand Down Expand Up @@ -109,6 +112,7 @@ class AutofillSettingsViewModel @Inject constructor(
private val syncEngine: SyncEngine,
private val neverSavedSiteRepository: NeverSavedSiteRepository,
private val capabilityChecker: InternalAutofillCapabilityChecker,
private val autofillSurvey: AutofillSurvey,
) : ViewModel() {

private val _viewState = MutableStateFlow(ViewState())
Expand Down Expand Up @@ -146,7 +150,16 @@ class AutofillSettingsViewModel @Inject constructor(
addCommand(ShowUserPasswordCopied())
}

fun onShowListMode() {
fun onInitialiseListMode() {
onShowListMode()
showSurveyIfAvailable()
}

fun onReturnToListModeFromCredentialMode() {
onShowListMode()
}

private fun onShowListMode() {
_viewState.value = _viewState.value.copy(credentialMode = ListMode)
addCommand(ShowListMode)
}
Expand Down Expand Up @@ -247,6 +260,31 @@ class AutofillSettingsViewModel @Inject constructor(
}
}

private fun showSurveyIfAvailable() {
viewModelScope.launch(dispatchers.io()) {
val survey = autofillSurvey.firstUnusedSurvey()
_viewState.value = _viewState.value.copy(survey = survey)

if (survey != null) {
pixel.fire(AutofillPixelNames.AUTOFILL_SURVEY_AVAILABLE_PROMPT_DISPLAYED)
}
}
}

fun onSurveyShown(surveyId: String) {
viewModelScope.launch(dispatchers.io()) {
_viewState.value = _viewState.value.copy(survey = null)
autofillSurvey.recordSurveyAsUsed(surveyId)
}
}

fun onSurveyPromptDismissed(surveyId: String) {
viewModelScope.launch(dispatchers.io()) {
_viewState.value = _viewState.value.copy(survey = null)
autofillSurvey.recordSurveyAsUsed(surveyId)
}
}

suspend fun launchDeviceAuth() {
if (!autofillStore.autofillAvailable) {
Timber.d("Can't access secure storage so can't offer autofill functionality")
Expand Down Expand Up @@ -317,7 +355,7 @@ class AutofillSettingsViewModel @Inject constructor(
}

private fun addCommand(command: Command) {
Timber.v("Adding command %s", command)
Timber.v("Adding command %s", command::class.simpleName)
commands.value.let { commands ->
val updatedList = commands + command
_commands.value = updatedList
Expand Down Expand Up @@ -647,6 +685,7 @@ class AutofillSettingsViewModel @Inject constructor(
val credentialMode: CredentialMode? = null,
val credentialSearchQuery: String = "",
val webViewCompatible: Boolean = true,
val survey: SurveyDetails? = null,
)

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* 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.ui.credential.management.survey

import androidx.core.net.toUri
import com.duckduckgo.app.statistics.store.StatisticsDataStore
import com.duckduckgo.app.usage.app.AppDaysUsedRepository
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.autofill.impl.configuration.integration.JavascriptCommunicationSupport
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurvey.SurveyDetails
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurveyImpl.Companion.SurveyParams.IN_APP
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurveyImpl.Companion.SurveyParams.NUMBER_PASSWORD_BUCKET_LOTS
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurveyImpl.Companion.SurveyParams.NUMBER_PASSWORD_BUCKET_MANY
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurveyImpl.Companion.SurveyParams.NUMBER_PASSWORD_BUCKET_NONE
import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurveyImpl.Companion.SurveyParams.NUMBER_PASSWORD_BUCKET_SOME
import com.duckduckgo.browser.api.UserBrowserProperties
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext

interface AutofillSurvey {
suspend fun firstUnusedSurvey(): SurveyDetails?
suspend fun recordSurveyAsUsed(id: String)

data class SurveyDetails(
val id: String,
val url: String,
)
}

@ContributesBinding(AppScope::class)
class AutofillSurveyImpl @Inject constructor(
private val statisticsStore: StatisticsDataStore,
private val userBrowserProperties: UserBrowserProperties,
private val appBuildConfig: AppBuildConfig,
private val appDaysUsedRepository: AppDaysUsedRepository,
private val dispatchers: DispatcherProvider,
private val autofillSurveyStore: AutofillSurveyStore,
private val javascriptCommunicationSupport: JavascriptCommunicationSupport,
private val internalAutofillStore: InternalAutofillStore,
) : AutofillSurvey {

override suspend fun firstUnusedSurvey(): SurveyDetails? {
if (!canShowSurvey()) return null
val survey = availableSurveys.firstOrNull { !surveyTakenPreviously(it.id) } ?: return null
return survey.copy(url = survey.url.addSurveyParameters())
}

private fun canShowSurvey(): Boolean {
return deviceSetToEnglish() && javascriptCommunicationSupport.supportsModernIntegration()
}

override suspend fun recordSurveyAsUsed(id: String) {
autofillSurveyStore.recordSurveyWasShown(id)
}

private fun deviceSetToEnglish(): Boolean {
return appBuildConfig.deviceLocale.language == Locale("en").language
}

private suspend fun surveyTakenPreviously(surveyId: String): Boolean {
return autofillSurveyStore.hasSurveyBeenTaken(surveyId)
}

private suspend fun String.addSurveyParameters(): String {
return withContext(dispatchers.io()) {
val urlBuilder = toUri()
.buildUpon()
.appendQueryParameter(SurveyParams.ATB, statisticsStore.atb?.version ?: "")
.appendQueryParameter(SurveyParams.ATB_VARIANT, statisticsStore.variant)
.appendQueryParameter(SurveyParams.DAYS_INSTALLED, "${userBrowserProperties.daysSinceInstalled()}")
.appendQueryParameter(SurveyParams.ANDROID_VERSION, "${appBuildConfig.sdkInt}")
.appendQueryParameter(SurveyParams.APP_VERSION, appBuildConfig.versionName)
.appendQueryParameter(SurveyParams.MANUFACTURER, appBuildConfig.manufacturer)
.appendQueryParameter(SurveyParams.MODEL, appBuildConfig.model)
.appendQueryParameter(SurveyParams.SOURCE, IN_APP)
.appendQueryParameter(SurveyParams.LAST_ACTIVE_DATE, appDaysUsedRepository.getLastActiveDay())
.appendQueryParameter(SurveyParams.NUMBER_PASSWORDS, bucketSavedPasswords(internalAutofillStore.getCredentialCount().firstOrNull()))

urlBuilder.build().toString()
}
}

private fun bucketSavedPasswords(passwordsSaved: Int?): String {
return when {
passwordsSaved == null -> NUMBER_PASSWORD_BUCKET_NONE
passwordsSaved < 3 -> NUMBER_PASSWORD_BUCKET_NONE
passwordsSaved < 10 -> NUMBER_PASSWORD_BUCKET_SOME
passwordsSaved < 50 -> NUMBER_PASSWORD_BUCKET_MANY
else -> NUMBER_PASSWORD_BUCKET_LOTS
}
}

companion object {
private val availableSurveys = listOf(
SurveyDetails(
id = "autofill-2024-04-26",
url = "https://selfserve.decipherinc.com/survey/selfserve/32ab/240308",
),
)

private object SurveyParams {
const val ATB = "atb"
const val ATB_VARIANT = "var"
const val DAYS_INSTALLED = "delta"
const val ANDROID_VERSION = "av"
const val APP_VERSION = "ddgv"
const val MANUFACTURER = "man"
const val MODEL = "mo"
const val LAST_ACTIVE_DATE = "da"
const val SOURCE = "src"
const val IN_APP = "in_app"
const val NUMBER_PASSWORDS = "saved_passwords"
const val NUMBER_PASSWORD_BUCKET_NONE = "none"
const val NUMBER_PASSWORD_BUCKET_SOME = "some"
const val NUMBER_PASSWORD_BUCKET_MANY = "many"
const val NUMBER_PASSWORD_BUCKET_LOTS = "lots"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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.ui.credential.management.survey

import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
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.withContext

interface AutofillSurveyStore {
suspend fun hasSurveyBeenTaken(id: String): Boolean
suspend fun recordSurveyWasShown(id: String)
}

@ContributesBinding(AppScope::class)
@SingleInstanceIn(AppScope::class)
class AutofillSurveyStoreImpl @Inject constructor(
private val context: Context,
private val dispatchers: DispatcherProvider,
) : AutofillSurveyStore {

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

override suspend fun hasSurveyBeenTaken(id: String): Boolean {
return withContext(dispatchers.io()) {
val previousSurveys = prefs.getStringSet(SURVEY_IDS, mutableSetOf()) ?: mutableSetOf()
previousSurveys.contains(id)
}
}

override suspend fun recordSurveyWasShown(id: String) {
withContext(dispatchers.io()) {
val currentValue = prefs.getStringSet(SURVEY_IDS, mutableSetOf()) ?: mutableSetOf()
val newValue = currentValue.toMutableSet()
newValue.add(id)
prefs.edit {
putStringSet(SURVEY_IDS, newValue)
}
}
}

companion object {
private const val PREFS_FILE_NAME = "autofill_survey_store"
private const val SURVEY_IDS = "survey_ids"
}
}

0 comments on commit c4dfe03

Please sign in to comment.