Skip to content

Commit

Permalink
For mozilla-mobile#7094 - Adds save login exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
ekager committed Jul 7, 2020
1 parent 4f74f34 commit dc0b0f9
Show file tree
Hide file tree
Showing 36 changed files with 772 additions and 150 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ dependencies {
implementation Deps.mozilla_feature_toolbar
implementation Deps.mozilla_feature_tabs
implementation Deps.mozilla_feature_findinpage
implementation Deps.mozilla_feature_logins
implementation Deps.mozilla_feature_site_permissions
implementation Deps.mozilla_feature_readerview
implementation Deps.mozilla_feature_tab_collections
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ class SettingsPrivacyTest {
verifyDefaultView()
verifyDefaultValueSyncLogins()
verifyDefaultValueAutofillLogins()
verifyDefaultValueExceptions()
}.openSavedLogins {
verifySavedLoginsView()
tapSetupLater()
Expand Down Expand Up @@ -209,13 +210,13 @@ class SettingsPrivacyTest {
}

@Test
fun doNotSaveLoginFromPromptTest() {
fun neverSaveLoginFromPromptTest() {
val saveLoginTest = TestAssetHelper.getSaveLoginAsset(mockWebServer)

navigationToolbar {
}.enterURLAndEnterToBrowser(saveLoginTest.url) {
verifySaveLoginPromptIsShown()
// Don't save the login
// Don't save the login, add to exceptions
saveLoginFromPrompt("Never save")
}.openTabDrawer {
}.openHomeScreen {
Expand All @@ -229,6 +230,10 @@ class SettingsPrivacyTest {
tapSetupLater()
// Verify that the login list is empty
verifyNotSavedLoginFromPromt()
}.goBack {
}.openLoginsExceptions {
// Verify localhost was added to exceptions list
verifyExceptionAdded()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class SettingsSubMenuLoginsAndPasswordRobot {
mDevice.waitNotNull(Until.findObjects(By.text("On")), TestAssetHelper.waitingTime)
}

fun verifyDefaultValueExceptions() = assertDefaultValueExceptions()

fun verifyDefaultValueAutofillLogins() = assertDefaultValueAutofillLogins()

fun verifyDefaultValueSyncLogins() = assertDefaultValueSyncLogins()
Expand All @@ -60,6 +62,14 @@ class SettingsSubMenuLoginsAndPasswordRobot {
return SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition()
}

fun openLoginsExceptions(interact: SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition {
fun loginExceptionsButton() = onView(ViewMatchers.withText("Exceptions"))
loginExceptionsButton().click()

SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot().interact()
return SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition()
}

fun openSyncLogins(interact: SettingsTurnOnSyncRobot.() -> Unit): SettingsTurnOnSyncRobot.Transition {
fun syncLoginsButton() = onView(ViewMatchers.withText("Sync logins"))
syncLoginsButton().click()
Expand Down Expand Up @@ -92,5 +102,8 @@ private fun assertDefaultView() = onView(ViewMatchers.withText("Sync logins"))
private fun assertDefaultValueAutofillLogins() = onView(ViewMatchers.withText("Autofill"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))

private fun assertDefaultValueExceptions() = onView(ViewMatchers.withText("Exceptions"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))

private fun assertDefaultValueSyncLogins() = onView(ViewMatchers.withText("Sign in to Sync"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.containsString
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.ext.waitNotNull

Expand All @@ -24,16 +25,23 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot {
fun verifySavedLoginsView() = assertSavedLoginsView()

fun verifySavedLoginsAfterSync() {
mDevice.waitNotNull(Until.findObjects(By.text("https://accounts.google.com")), TestAssetHelper.waitingTime)
mDevice.waitNotNull(
Until.findObjects(By.text("https://accounts.google.com")),
TestAssetHelper.waitingTime
)
assertSavedLoginAppears()
}

fun tapSetupLater() = onView(ViewMatchers.withText("Later")).perform(ViewActions.click())

fun verifySavedLoginFromPrompt() = mDevice.waitNotNull(Until.findObjects(By.text("test@example.com")))
fun verifySavedLoginFromPrompt() =
mDevice.waitNotNull(Until.findObjects(By.text("test@example.com")))

fun verifyNotSavedLoginFromPromt() = onView(ViewMatchers.withText("test@example.com"))
.check(ViewAssertions.doesNotExist())
.check(ViewAssertions.doesNotExist())

fun verifyExceptionAdded() = onView(ViewMatchers.withText(containsString("localhost")))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))

class Transition {
fun goBack(interact: SettingsSubMenuLoginsAndPasswordRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordRobot.Transition {
Expand All @@ -46,9 +54,10 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot {
}

private fun goBackButton() =
onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))
onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))

private fun assertSavedLoginsView() = onView(ViewMatchers.withText("Secure your logins and passwords"))
private fun assertSavedLoginsView() =
onView(ViewMatchers.withText("Secure your logins and passwords"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))

private fun assertSavedLoginAppears() = onView(ViewMatchers.withText("https://accounts.google.com"))
2 changes: 1 addition & 1 deletion app/src/main/java/org/mozilla/fenix/HomeActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.exceptions.ExceptionsFragmentDirections
import org.mozilla.fenix.trackingprotectionexceptions.ExceptionsFragmentDirections
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
isSaveLoginEnabled = {
context.settings().shouldPromptToSaveLogins
},
loginExceptionStorage = context.components.core.loginExceptionStorage,
shareDelegate = object : ShareDelegate {
override fun showShareSheet(
context: Context,
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/org/mozilla/fenix/components/Core.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.fetch.Client
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
import mozilla.components.feature.downloads.DownloadMiddleware
import mozilla.components.feature.logins.exceptions.LoginExceptionStorage
import mozilla.components.feature.media.RecordingDevicesNotificationFeature
import mozilla.components.feature.media.middleware.MediaMiddleware
import mozilla.components.feature.pwa.ManifestStorage
Expand Down Expand Up @@ -251,6 +252,8 @@ class Core(private val context: Context) {

val webAppManifestStorage by lazy { ManifestStorage(context) }

val loginExceptionStorage by lazy { LoginExceptionStorage(context) }

/**
* Shared Preferences that encrypt/decrypt using Android KeyStore and lib-dataprotect for 23+
* only on Nightly/Debug for now, otherwise simply stored.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.loginexceptions

import mozilla.components.feature.logins.exceptions.LoginException
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store

/**
* The [Store] for holding the [ExceptionsFragmentState] and applying [ExceptionsFragmentAction]s.
*/
class ExceptionsFragmentStore(initialState: ExceptionsFragmentState) :
Store<ExceptionsFragmentState, ExceptionsFragmentAction>(initialState, ::exceptionsStateReducer)

/**
* Actions to dispatch through the `ExceptionsStore` to modify `ExceptionsState` through the reducer.
*/
sealed class ExceptionsFragmentAction : Action {
data class Change(val list: List<LoginException>) : ExceptionsFragmentAction()
}

/**
* The state for the Exceptions Screen
* @property items List of exceptions to display
*/
data class ExceptionsFragmentState(val items: List<LoginException>) : State

/**
* The ExceptionsState Reducer.
*/
private fun exceptionsStateReducer(
state: ExceptionsFragmentState,
action: ExceptionsFragmentAction
): ExceptionsFragmentState {
return when (action) {
is ExceptionsFragmentAction.Change -> state.copy(items = action.list)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.loginexceptions

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.feature.logins.exceptions.LoginException
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsHeaderViewHolder
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsListItemViewHolder

sealed class AdapterItem {
object DeleteButton : AdapterItem()
object Header : AdapterItem()
data class Item(val item: LoginException) : AdapterItem()
}

/**
* Adapter for a list of sites that are exempted from saving logins,
* along with controls to remove the exception.
*/
class LoginExceptionsAdapter(
private val interactor: LoginExceptionsInteractor
) : ListAdapter<AdapterItem, RecyclerView.ViewHolder>(DiffCallback) {

/**
* Change the list of items that are displayed.
* Header and footer items are added to the list as well.
*/
fun updateData(exceptions: List<LoginException>) {
val adapterItems = mutableListOf<AdapterItem>()
adapterItems.add(AdapterItem.Header)
exceptions.mapTo(adapterItems) { AdapterItem.Item(it) }
adapterItems.add(AdapterItem.DeleteButton)
submitList(adapterItems)
}

override fun getItemViewType(position: Int) = when (getItem(position)) {
AdapterItem.DeleteButton -> LoginExceptionsDeleteButtonViewHolder.LAYOUT_ID
AdapterItem.Header -> LoginExceptionsHeaderViewHolder.LAYOUT_ID
is AdapterItem.Item -> LoginExceptionsListItemViewHolder.LAYOUT_ID
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)

return when (viewType) {
LoginExceptionsDeleteButtonViewHolder.LAYOUT_ID -> LoginExceptionsDeleteButtonViewHolder(
view,
interactor
)
LoginExceptionsHeaderViewHolder.LAYOUT_ID -> LoginExceptionsHeaderViewHolder(view)
LoginExceptionsListItemViewHolder.LAYOUT_ID -> LoginExceptionsListItemViewHolder(
view,
interactor
)
else -> throw IllegalStateException()
}
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is LoginExceptionsListItemViewHolder) {
val adapterItem = getItem(position) as AdapterItem.Item
holder.bind(adapterItem.item)
}
}

private object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
areContentsTheSame(oldItem, newItem)

@Suppress("DiffUtilEquals")
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
oldItem == newItem
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.loginexceptions

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import kotlinx.android.synthetic.main.fragment_exceptions.view.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import mozilla.components.feature.logins.exceptions.LoginException
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.R
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.showToolbar

/**
* Displays a list of sites that are exempted from saving logins,
* along with controls to remove the exception.
*/
class LoginExceptionsFragment : Fragment() {
private lateinit var exceptionsStore: ExceptionsFragmentStore
private lateinit var exceptionsView: LoginExceptionsView
private lateinit var exceptionsInteractor: LoginExceptionsInteractor

override fun onResume() {
super.onResume()
showToolbar(getString(R.string.preference_exceptions))
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_exceptions, container, false)
exceptionsStore = StoreProvider.get(this) {
ExceptionsFragmentStore(
ExceptionsFragmentState(
items = listOf()
)
)
}
exceptionsInteractor =
LoginExceptionsInteractor(::deleteOneItem, ::deleteAllItems)
exceptionsView = LoginExceptionsView(view.exceptionsLayout, exceptionsInteractor)
subscribeToLoginExceptions()
return view
}

private fun subscribeToLoginExceptions(): Observer<List<LoginException>> {
return Observer<List<LoginException>> { exceptions ->
exceptionsStore.dispatch(ExceptionsFragmentAction.Change(exceptions))
}.also { observer ->
requireComponents.core.loginExceptionStorage.getLoginExceptions().asLiveData()
.observe(viewLifecycleOwner, observer)
}
}

@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
consumeFrom(exceptionsStore) {
exceptionsView.update(it)
}
}

private fun deleteAllItems() {
viewLifecycleOwner.lifecycleScope.launch(IO) {
requireComponents.core.loginExceptionStorage.deleteAllLoginExceptions()
}
}

private fun deleteOneItem(item: LoginException) {
viewLifecycleOwner.lifecycleScope.launch(IO) {
requireComponents.core.loginExceptionStorage.removeLoginException(item)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.loginexceptions

import mozilla.components.feature.logins.exceptions.LoginException

/**
* Interactor for the exceptions screen
* Provides implementations for the ExceptionsViewInteractor
*/
class LoginExceptionsInteractor(
private val deleteOne: (LoginException) -> Unit,
private val deleteAll: () -> Unit
) : ExceptionsViewInteractor {
override fun onDeleteAll() {
deleteAll.invoke()
}

override fun onDeleteOne(item: LoginException) {
deleteOne.invoke(item)
}
}
Loading

0 comments on commit dc0b0f9

Please sign in to comment.