Skip to content

Commit

Permalink
Autofill engine: support saving and updating last updated time and no…
Browse files Browse the repository at this point in the history
…tes along with credentials (#2101)


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

### Description
This PR includes:
- Saving of last updated time upon saving of the credential
- Update of last updated time (UI and DB) when saving edits to a credential
- Update of notes and domain title(UI and DB) when saving edits to a credential
- Move reference to currently being edited credential from the CredentialModeFragment to the ViewModel removing any reference to the state from the fragment.


### Steps to test this PR

_Save credential saves last updated time_
- [x] Open a website
- [x] Save credential
- [x] Go to autofill management screen and open the credential you saved
- [x] Confirm that last updated time is displayed and is correct

_Updating credentials persists_
- [x] Go to autofill management screen and select any saved credential
- [x] Select edit from the context menu
- [x] Update all fields
- [x] Press the check icon to save the changes
- [x] Confirm that view mode contains all updated data and the last updated time is updated. If you changed the domain, it is possible that the icon in the toolbar will not show if we haven't loaded the icon yet. For icons that already exist, it should change automatically.
- [x] Exit autofill management screen and comeback
- [x] Confirm that edited credentials is persisted
  • Loading branch information
karlenDimla authored Jul 28, 2022
1 parent 30ff63b commit 5dd7fc2
Show file tree
Hide file tree
Showing 13 changed files with 209 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ interface AutofillStore {
*/
suspend fun getCredentials(rawUrl: String): List<LoginCredentials>

/**
* Find saved credential for the given id
* @param id of the saved credential
*/
suspend fun getCredentialsWithId(id: Int): LoginCredentials?

/**
* Save the given credentials for the given URL
* @param rawUrl Can be a full, unmodified URL taken from the URL bar (containing subdomains, query params etc...)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.duckduckgo.autofill.InternalTestUserChecker
import com.duckduckgo.autofill.store.AutofillStore
import com.duckduckgo.autofill.store.InternalTestUserStore
import com.duckduckgo.autofill.store.RealInternalTestUserStore
import com.duckduckgo.autofill.store.RealLastUpdatedTimeProvider
import com.duckduckgo.autofill.store.SecureStoreBackedAutofillStore
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.securestorage.api.SecureStorage
Expand All @@ -43,6 +44,6 @@ class AutofillModule {
context: Context,
internalTestUserChecker: InternalTestUserChecker
): AutofillStore {
return SecureStoreBackedAutofillStore(secureStorage, context, internalTestUserChecker)
return SecureStoreBackedAutofillStore(secureStorage, context, internalTestUserChecker, RealLastUpdatedTimeProvider())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ import com.duckduckgo.autofill.impl.R
import com.duckduckgo.autofill.impl.databinding.ActivityAutofillSettingsBinding
import com.duckduckgo.autofill.ui.AutofillSettingsActivityLauncher
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.Command.*
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.CredentialModeState.Editing
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.CredentialModeState.Viewing
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.CredentialMode.Editing
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.CredentialMode.Viewing
import com.duckduckgo.autofill.ui.credential.management.viewing.AutofillManagementDisabledMode
import com.duckduckgo.autofill.ui.credential.management.viewing.AutofillManagementCredentialsMode
import com.duckduckgo.autofill.ui.credential.management.viewing.AutofillManagementLockedMode
Expand Down Expand Up @@ -153,7 +153,7 @@ class AutofillManagementActivity : DuckDuckGoActivity() {
title = credentials.domainTitle ?: credentials.domain
supportFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.fragment_container_view, AutofillManagementCredentialsMode.instance(credentials))
replace(R.id.fragment_container_view, AutofillManagementCredentialsMode.instance())
}
}
}
Expand Down Expand Up @@ -184,8 +184,8 @@ class AutofillManagementActivity : DuckDuckGoActivity() {
}

override fun onBackPressed() {
when (viewModel.viewState.value.credentialModeState) {
Editing -> viewModel.onExitEditMode(true)
when (viewModel.viewState.value.credentialMode) {
is Editing -> viewModel.onCancelEditMode()
is Viewing -> viewModel.onExitViewMode()
else -> super.onBackPressed()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.autofill.domain.app.LoginCredentials
import com.duckduckgo.autofill.store.AutofillStore
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.Command.*
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.CredentialModeState.Editing
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.CredentialModeState.NotInCredentialMode
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.CredentialModeState.Viewing
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.CredentialMode.Editing
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.CredentialMode.NotInCredentialMode
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.CredentialMode.Viewing
import com.duckduckgo.di.scopes.ActivityScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand Down Expand Up @@ -56,12 +56,14 @@ class AutofillSettingsViewModel @Inject constructor(
}

fun onViewCredentials(credentials: LoginCredentials) {
_viewState.value = viewState.value.copy(credentialModeState = Viewing())
_viewState.value = viewState.value.copy(credentialMode = Viewing(credentialsViewed = credentials))
addCommand(ShowCredentialMode(credentials))
}

fun onEditCredentials() {
_viewState.value = viewState.value.copy(credentialModeState = Editing)
viewState.value.credentialMode.credentialsViewed?.let {
_viewState.value = viewState.value.copy(credentialMode = Editing(credentialsViewed = it))
}
}

fun launchDeviceAuth() {
Expand All @@ -75,12 +77,12 @@ class AutofillSettingsViewModel @Inject constructor(
}

fun unlock() {
_viewState.value = viewState.value.copy(isLocked = false, credentialModeState = NotInCredentialMode)
_viewState.value = viewState.value.copy(isLocked = false, credentialMode = NotInCredentialMode)
addCommand(ShowListMode)
}

fun disabled() {
_viewState.value = viewState.value.copy(isLocked = true, credentialModeState = NotInCredentialMode)
_viewState.value = viewState.value.copy(isLocked = true, credentialMode = NotInCredentialMode)
addCommand(ShowDisabledMode)
}

Expand Down Expand Up @@ -111,18 +113,30 @@ class AutofillSettingsViewModel @Inject constructor(
}
}

fun onDeleteCredentials(credentials: LoginCredentials) {
val credentialsId = credentials.id ?: return
fun onDeleteCredentials() {
_viewState.value.credentialMode.credentialsViewed?.let {
val credentialsId = it.id ?: return

viewModelScope.launch {
autofillStore.deleteCredentials(credentialsId)
viewModelScope.launch {
autofillStore.deleteCredentials(credentialsId)
}
}

onExitViewMode()
}

fun updateCredentials(updatedCredentials: LoginCredentials) {
viewModelScope.launch {
autofillStore.updateCredentials(updatedCredentials)
_viewState.value.credentialMode.credentialsViewed?.let {
viewModelScope.launch {
autofillStore.updateCredentials(updatedCredentials.copy(id = it.id))
autofillStore.getCredentialsWithId(it.id!!)?.let { credentials ->
_viewState.value = viewState.value.copy(
credentialMode = Viewing(
credentialsViewed = credentials
)
)
}
}
}
}

Expand All @@ -136,26 +150,28 @@ class AutofillSettingsViewModel @Inject constructor(
_viewState.value = viewState.value.copy(autofillEnabled = false)
}

fun onExitEditMode(shouldReset: Boolean) {
_viewState.value = viewState.value.copy(credentialModeState = Viewing(reset = shouldReset))
fun onCancelEditMode() {
viewState.value.credentialMode.credentialsViewed?.let {
_viewState.value = viewState.value.copy(credentialMode = Viewing(credentialsViewed = it))
}
}

fun onExitViewMode() {
_viewState.value = viewState.value.copy(credentialModeState = NotInCredentialMode)
_viewState.value = viewState.value.copy(credentialMode = NotInCredentialMode)
addCommand(ShowListMode)
}

data class ViewState(
val autofillEnabled: Boolean = true,
val logins: List<LoginCredentials> = emptyList(),
val credentialModeState: CredentialModeState = NotInCredentialMode,
val credentialMode: CredentialMode = NotInCredentialMode,
val isLocked: Boolean = false
)

sealed class CredentialModeState {
data class Viewing(val reset: Boolean = false) : CredentialModeState()
object Editing : CredentialModeState()
object NotInCredentialMode : CredentialModeState()
sealed class CredentialMode(open val credentialsViewed: LoginCredentials?) {
data class Viewing(override val credentialsViewed: LoginCredentials) : CredentialMode(credentialsViewed)
data class Editing(override val credentialsViewed: LoginCredentials) : CredentialMode(credentialsViewed)
object NotInCredentialMode : CredentialMode(null)
}

sealed class Command(val id: String = UUID.randomUUID().toString()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ import com.duckduckgo.autofill.domain.app.LoginCredentials
import com.duckduckgo.autofill.impl.R
import com.duckduckgo.autofill.impl.databinding.FragmentAutofillManagementEditModeBinding
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.CredentialModeState.Editing
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.CredentialModeState.Viewing
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.CredentialMode.Editing
import com.duckduckgo.autofill.ui.credential.management.AutofillSettingsViewModel.CredentialMode.Viewing
import com.duckduckgo.di.scopes.FragmentScope
import com.duckduckgo.mobile.android.ui.view.OutLinedTextInputView
import dagger.android.support.AndroidSupportInjection
Expand All @@ -65,7 +65,6 @@ class AutofillManagementCredentialsMode : Fragment(), MenuProvider {
}

private lateinit var binding: FragmentAutofillManagementEditModeBinding
private lateinit var credentials: LoginCredentials

override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
Expand All @@ -83,11 +82,11 @@ class AutofillManagementCredentialsMode : Fragment(), MenuProvider {
}

override fun onPrepareMenu(menu: Menu) {
if (viewModel.viewState.value.credentialModeState == Editing) {
if (viewModel.viewState.value.credentialMode is Editing) {
menu.findItem(R.id.view_menu_save).isVisible = true
menu.findItem(R.id.view_menu_delete).isVisible = false
menu.findItem(R.id.view_menu_edit).isVisible = false
} else if (viewModel.viewState.value.credentialModeState is Viewing) {
} else if (viewModel.viewState.value.credentialMode is Viewing) {
menu.findItem(R.id.view_menu_save).isVisible = false
menu.findItem(R.id.view_menu_delete).isVisible = true
menu.findItem(R.id.view_menu_edit).isVisible = true
Expand All @@ -108,7 +107,7 @@ class AutofillManagementCredentialsMode : Fragment(), MenuProvider {
true
}
R.id.view_menu_delete -> {
viewModel.onDeleteCredentials(credentials)
viewModel.onDeleteCredentials()
true
}
R.id.view_menu_save -> {
Expand All @@ -124,11 +123,8 @@ class AutofillManagementCredentialsMode : Fragment(), MenuProvider {
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
credentials = requireArguments().getParcelable(EXTRA_KEY_CREDENTIALS)!!
observeViewModel()
populateFields()
configureUiEventHandlers()
loadDomainFavicon()
}

override fun onDestroyView() {
Expand All @@ -155,19 +151,18 @@ class AutofillManagementCredentialsMode : Fragment(), MenuProvider {
}

private fun saveCredentials() {
val updatedCredentials = credentials.copy(
val updatedCredentials = LoginCredentials(
username = binding.usernameEditText.text.convertBlankToNull(),
password = binding.passwordEditText.text.convertBlankToNull(),
domain = binding.domainEditText.text.convertBlankToNull(),
domainTitle = binding.domainTitleEditText.text.convertBlankToNull(),
notes = binding.notesEditText.text.convertBlankToNull()
)
credentials = updatedCredentials
viewModel.updateCredentials(updatedCredentials)
viewModel.onExitEditMode(false)
}

private fun populateFields() {
private fun populateFields(credentials: LoginCredentials) {
loadDomainFavicon(credentials)
binding.apply {
domainTitleEditText.setText(credentials.domainTitle)
usernameEditText.setText(credentials.username)
Expand All @@ -180,8 +175,8 @@ class AutofillManagementCredentialsMode : Fragment(), MenuProvider {
}
}

private fun showViewMode() {
updateToolbarForView()
private fun showViewMode(credentials: LoginCredentials) {
updateToolbarForView(credentials)
binding.apply {
domainTitleEditText.visibility = View.GONE
domainTitleEditText.isEditable = false
Expand Down Expand Up @@ -212,12 +207,12 @@ class AutofillManagementCredentialsMode : Fragment(), MenuProvider {
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.viewState.collect { state ->
when (state.credentialModeState) {
when (state.credentialMode) {
is Viewing -> {
if (state.credentialModeState.reset) populateFields()
showViewMode()
populateFields(state.credentialMode.credentialsViewed)
showViewMode(state.credentialMode.credentialsViewed)
}
Editing -> showEditMode()
is Editing -> showEditMode()
else -> {
}
}
Expand All @@ -237,7 +232,7 @@ class AutofillManagementCredentialsMode : Fragment(), MenuProvider {
invalidateMenu()
}

private fun updateToolbarForView() {
private fun updateToolbarForView(credentials: LoginCredentials) {
getActionBar()?.apply {
setHomeAsUpIndicator(com.duckduckgo.mobile.android.R.drawable.ic_back_24)
title = credentials.domainTitle ?: credentials.domain
Expand All @@ -246,7 +241,7 @@ class AutofillManagementCredentialsMode : Fragment(), MenuProvider {
invalidateMenu()
}

private fun loadDomainFavicon() {
private fun loadDomainFavicon(credentials: LoginCredentials) {
lifecycleScope.launch {
credentials.domain?.let {
getActionBar()?.setLogo(
Expand All @@ -270,13 +265,6 @@ class AutofillManagementCredentialsMode : Fragment(), MenuProvider {

companion object {

private const val EXTRA_KEY_CREDENTIALS = "credentials"

fun instance(credentials: LoginCredentials) =
AutofillManagementCredentialsMode().apply {
arguments = Bundle().apply {
putParcelable(EXTRA_KEY_CREDENTIALS, credentials)
}
}
fun instance() = AutofillManagementCredentialsMode()
}
}
Loading

0 comments on commit 5dd7fc2

Please sign in to comment.