1
0
Fork 0

For #7094 - Adds save login exceptions

master
ekager 2020-07-01 22:37:03 -04:00 committed by Emily Kager
parent 69020a1f26
commit 9ed85510ae
41 changed files with 773 additions and 160 deletions

View File

@ -501,6 +501,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

View File

@ -170,6 +170,7 @@ class SettingsPrivacyTest {
verifyDefaultView()
verifyDefaultValueSyncLogins()
verifyDefaultValueAutofillLogins()
verifyDefaultValueExceptions()
}.openSavedLogins {
verifySavedLoginsView()
tapSetupLater()
@ -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 {
@ -228,7 +229,11 @@ class SettingsPrivacyTest {
verifySavedLoginsView()
tapSetupLater()
// Verify that the login list is empty
verifyNotSavedLoginFromPromt()
verifyNotSavedLoginFromPrompt()
}.goBack {
}.openLoginExceptions {
// Verify localhost was added to exceptions list
verifyLocalhostExceptionAdded()
}
}

View File

@ -38,6 +38,8 @@ class SettingsSubMenuLoginsAndPasswordRobot {
mDevice.waitNotNull(Until.findObjects(By.text("On")), TestAssetHelper.waitingTime)
}
fun verifyDefaultValueExceptions() = assertDefaultValueExceptions()
fun verifyDefaultValueAutofillLogins() = assertDefaultValueAutofillLogins()
fun verifyDefaultValueSyncLogins() = assertDefaultValueSyncLogins()
@ -60,6 +62,14 @@ class SettingsSubMenuLoginsAndPasswordRobot {
return SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.Transition()
}
fun openLoginExceptions(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()
@ -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)))

View File

@ -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
@ -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())
fun verifyNotSavedLoginFromPrompt() = onView(ViewMatchers.withText("test@example.com"))
.check(ViewAssertions.doesNotExist())
fun verifyLocalhostExceptionAdded() = onView(ViewMatchers.withText(containsString("localhost")))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
class Transition {
fun goBack(interact: SettingsSubMenuLoginsAndPasswordRobot.() -> Unit): SettingsSubMenuLoginsAndPasswordRobot.Transition {
@ -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"))

View File

@ -21,7 +21,7 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromSyncedTabs(R.id.syncedTabsFragment),
FromBookmarks(R.id.bookmarkFragment),
FromHistory(R.id.historyFragment),
FromExceptions(R.id.exceptionsFragment),
FromTrackingProtectionExceptions(R.id.trackingProtectionExceptionsFragment),
FromAbout(R.id.aboutFragment),
FromTrackingProtection(R.id.trackingProtectionFragment),
FromSavedLoginsFragment(R.id.savedLoginsFragment),

View File

@ -62,7 +62,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.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
@ -498,8 +498,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
BookmarkFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHistory ->
HistoryFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromExceptions ->
ExceptionsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtectionExceptions ->
TrackingProtectionExceptionsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAbout ->
AboutFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtection ->

View File

@ -443,6 +443,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
isSaveLoginEnabled = {
context.settings().shouldPromptToSaveLogins
},
loginExceptionStorage = context.components.core.loginExceptionStorage,
shareDelegate = object : ShareDelegate {
override fun showShareSheet(
context: Context,

View File

@ -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
@ -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.

View File

@ -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)
}
}

View File

@ -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: List<AdapterItem> =
listOf(AdapterItem.Header) + exceptions.map { AdapterItem.Item(it) } + listOf(
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
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -0,0 +1,62 @@
/* 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 android.widget.FrameLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_exceptions.view.*
import mozilla.components.feature.logins.exceptions.LoginException
import org.mozilla.fenix.R
/**
* Interface for the ExceptionsViewInteractor. This interface is implemented by objects that want
* to respond to user interaction on the ExceptionsView
*/
interface ExceptionsViewInteractor {
/**
* Called whenever all exception items are deleted
*/
fun onDeleteAll()
/**
* Called whenever one exception item is deleted
*/
fun onDeleteOne(item: LoginException)
}
/**
* View that contains and configures the Exceptions List
*/
class LoginExceptionsView(
override val containerView: ViewGroup,
val interactor: LoginExceptionsInteractor
) : LayoutContainer {
val view: FrameLayout = LayoutInflater.from(containerView.context)
.inflate(R.layout.component_exceptions, containerView, true)
.findViewById(R.id.exceptions_wrapper)
private val exceptionsAdapter = LoginExceptionsAdapter(interactor)
init {
view.exceptions_learn_more.isVisible = false
view.exceptions_empty_message.text =
view.context.getString(R.string.preferences_passwords_exceptions_description_empty)
view.exceptions_list.apply {
adapter = exceptionsAdapter
layoutManager = LinearLayoutManager(containerView.context)
}
}
fun update(state: ExceptionsFragmentState) {
view.exceptions_empty_view.isVisible = state.items.isEmpty()
view.exceptions_list.isVisible = state.items.isNotEmpty()
exceptionsAdapter.updateData(state.items)
}
}

View File

@ -0,0 +1,28 @@
/* 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.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.delete_exceptions_button.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.loginexceptions.LoginExceptionsInteractor
class LoginExceptionsDeleteButtonViewHolder(
view: View,
private val interactor: LoginExceptionsInteractor
) : RecyclerView.ViewHolder(view) {
private val deleteButton = view.removeAllExceptions
init {
deleteButton.setOnClickListener {
interactor.onDeleteAll()
}
}
companion object {
const val LAYOUT_ID = R.layout.delete_logins_exceptions_button
}
}

View File

@ -0,0 +1,23 @@
/* 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.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.exceptions_description.view.*
import org.mozilla.fenix.R
class LoginExceptionsHeaderViewHolder(
view: View
) : RecyclerView.ViewHolder(view) {
companion object {
const val LAYOUT_ID = R.layout.exceptions_description
}
init {
view.exceptions_description.text =
view.context.getString(R.string.preferences_passwords_exceptions_description)
}
}

View File

@ -0,0 +1,50 @@
/* 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.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.exception_item.view.*
import mozilla.components.feature.logins.exceptions.LoginException
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.loginexceptions.LoginExceptionsInteractor
/**
* View holder for a single website that is exempted from Tracking Protection.
*/
class LoginExceptionsListItemViewHolder(
view: View,
private val interactor: LoginExceptionsInteractor
) : RecyclerView.ViewHolder(view) {
private val favicon = view.favicon_image
private val url = view.webAddressView
private val deleteButton = view.delete_exception
private var item: LoginException? = null
init {
deleteButton.setOnClickListener {
item?.let {
interactor.onDeleteOne(it)
}
}
}
fun bind(item: LoginException) {
this.item = item
url.text = item.origin
}
private fun updateFavIcon(url: String) {
favicon.context.components.core.icons.loadIntoView(favicon, url)
}
companion object {
const val LAYOUT_ID = R.layout.exception_item
}
}

View File

@ -25,7 +25,7 @@ import org.mozilla.fenix.utils.view.addToRadioGroup
/**
* Displays the toggle for tracking protection, options for tracking protection policy and a button
* to open info about the tracking protection [org.mozilla.fenix.exceptions.ExceptionsFragment].
* to open info about the tracking protection [org.mozilla.fenix.settings.TrackingProtectionFragment].
*/
class TrackingProtectionFragment : PreferenceFragmentCompat() {
@ -56,7 +56,8 @@ class TrackingProtectionFragment : PreferenceFragmentCompat() {
showToolbar(getString(R.string.preference_enhanced_tracking_protection))
// Tracking Protection Switch
val preferenceTP = requirePreference<SwitchPreference>(R.string.pref_key_tracking_protection)
val preferenceTP =
requirePreference<SwitchPreference>(R.string.pref_key_tracking_protection)
preferenceTP.isChecked = requireContext().settings().shouldUseTrackingProtection
preferenceTP.setOnPreferenceChangeListener<Boolean> { preference, trackingProtectionOn ->
@ -86,7 +87,8 @@ class TrackingProtectionFragment : PreferenceFragmentCompat() {
getString(R.string.app_name)
)
val preferenceExceptions = requirePreference<Preference>(R.string.pref_key_tracking_protection_exceptions)
val preferenceExceptions =
requirePreference<Preference>(R.string.pref_key_tracking_protection_exceptions)
preferenceExceptions.onPreferenceClickListener = exceptionsClickListener
}

View File

@ -133,6 +133,13 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat(), AccountObserver {
}
}
requirePreference<Preference>(R.string.pref_key_login_exceptions).apply {
setOnPreferenceClickListener {
navigateToLoginExceptionFragment()
true
}
}
requirePreference<SwitchPreference>(R.string.pref_key_autofill_logins).apply {
isChecked = context.settings().shouldAutofillLogins
onPreferenceChangeListener = object : SharedPreferenceUpdater() {
@ -322,6 +329,12 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat(), AccountObserver {
findNavController().navigate(directions)
}
private fun navigateToLoginExceptionFragment() {
val directions =
SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToLoginExceptionsFragment()
findNavController().navigate(directions)
}
companion object {
const val SHORT_DELAY_MS = 100L
private const val LOG_TAG = "LoginsFragment"

View File

@ -2,7 +2,7 @@
* 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.exceptions
package org.mozilla.fenix.trackingprotectionexceptions
import android.view.LayoutInflater
import android.view.ViewGroup
@ -10,9 +10,9 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
import org.mozilla.fenix.exceptions.viewholders.ExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsHeaderViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsListItemViewHolder
import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsHeaderViewHolder
import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsListItemViewHolder
/**
* Adapter for a list of sites that are exempted from Tracking Protection,

View File

@ -2,7 +2,7 @@
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.exceptions
package org.mozilla.fenix.trackingprotectionexceptions
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
import mozilla.components.lib.state.Action

View File

@ -2,7 +2,7 @@
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.exceptions
package org.mozilla.fenix.trackingprotectionexceptions
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException

View File

@ -2,7 +2,7 @@
* 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.exceptions
package org.mozilla.fenix.trackingprotectionexceptions
import android.text.method.LinkMovementMethod
import android.text.style.UnderlineSpan
@ -50,7 +50,10 @@ class ExceptionsView(
.inflate(R.layout.component_exceptions, container, true)
.findViewById(R.id.exceptions_wrapper)
private val exceptionsAdapter = ExceptionsAdapter(interactor)
private val exceptionsAdapter =
ExceptionsAdapter(
interactor
)
init {
exceptions_list.apply {

View File

@ -2,7 +2,7 @@
* 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.exceptions
package org.mozilla.fenix.trackingprotectionexceptions
import android.os.Bundle
import android.util.Log
@ -27,7 +27,7 @@ import org.mozilla.fenix.settings.SupportUtils
* Displays a list of sites that are exempted from Tracking Protection,
* along with controls to remove the exception.
*/
class ExceptionsFragment : Fragment() {
class TrackingProtectionExceptionsFragment : Fragment() {
private lateinit var exceptionsStore: ExceptionsFragmentStore
private lateinit var exceptionsView: ExceptionsView
@ -54,8 +54,16 @@ class ExceptionsFragment : Fragment() {
)
}
exceptionsInteractor =
ExceptionsInteractor(::openLearnMore, ::deleteOneItem, ::deleteAllItems)
exceptionsView = ExceptionsView(view.exceptionsLayout, exceptionsInteractor)
ExceptionsInteractor(
::openLearnMore,
::deleteOneItem,
::deleteAllItems
)
exceptionsView =
ExceptionsView(
view.exceptionsLayout,
exceptionsInteractor
)
reloadExceptions()
return view
}
@ -83,13 +91,17 @@ class ExceptionsFragment : Fragment() {
searchTermOrURL = SupportUtils.getGenericSumoURLForTopic
(SupportUtils.SumoTopic.TRACKING_PROTECTION),
newTab = true,
from = BrowserDirection.FromExceptions
from = BrowserDirection.FromTrackingProtectionExceptions
)
}
private fun reloadExceptions() {
trackingProtectionUseCases.fetchExceptions { resultList ->
exceptionsStore.dispatch(ExceptionsFragmentAction.Change(resultList))
exceptionsStore.dispatch(
ExceptionsFragmentAction.Change(
resultList
)
)
}
}
}

View File

@ -2,13 +2,13 @@
* 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.exceptions.viewholders
package org.mozilla.fenix.trackingprotectionexceptions.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.delete_exceptions_button.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.exceptions.ExceptionsInteractor
import org.mozilla.fenix.trackingprotectionexceptions.ExceptionsInteractor
class ExceptionsDeleteButtonViewHolder(
view: View,

View File

@ -2,7 +2,7 @@
* 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.exceptions.viewholders
package org.mozilla.fenix.trackingprotectionexceptions.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView

View File

@ -2,14 +2,14 @@
* 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.exceptions.viewholders
package org.mozilla.fenix.trackingprotectionexceptions.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.exception_item.view.*
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
import org.mozilla.fenix.R
import org.mozilla.fenix.exceptions.ExceptionsInteractor
import org.mozilla.fenix.trackingprotectionexceptions.ExceptionsInteractor
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/removeAllExceptions"
style="@style/DestructiveButton"
android:layout_marginHorizontal="16dp"
android:text="@string/preferences_passwords_exceptions_remove_all" />

View File

@ -3,10 +3,7 @@
- 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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/exceptionsLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="org.mozilla.fenix.exceptions.ExceptionsFragment">
</LinearLayout>
android:orientation="vertical"/>

View File

@ -27,35 +27,83 @@
android:id="@+id/action_global_search"
app:destination="@id/searchFragment" />
<action android:id="@+id/action_global_shareFragment" app:destination="@id/shareFragment" />
<action android:id="@+id/action_global_crash_reporter" app:destination="@id/crashReporterFragment" />
<action android:id="@+id/action_global_turn_on_sync" app:destination="@id/turnOnSyncFragment" />
<action android:id="@+id/action_global_settings_addonsManagementFragment" app:destination="@id/addonsManagementFragment" />
<action android:id="@+id/action_global_searchEngineFragment" app:destination="@id/searchEngineFragment" />
<action android:id="@+id/action_global_accessibilityFragment" app:destination="@id/accessibilityFragment" />
<action android:id="@+id/action_global_deleteBrowsingDataFragment" app:destination="@id/deleteBrowsingDataFragment" />
<action android:id="@+id/action_global_webExtensionActionPopupFragment" app:destination="@id/webExtensionActionPopupFragment" />
<action android:id="@+id/action_global_settingsFragment" app:destination="@id/settingsFragment" />
<action android:id="@+id/action_global_syncedTabsFragment" app:destination="@+id/syncedTabsFragment" />
<action android:id="@+id/action_global_privateBrowsingFragment" app:destination="@id/privateBrowsingFragment"/>
<action android:id="@+id/action_global_bookmarkFragment" app:destination="@id/bookmarkFragment"/>
<action android:id="@+id/action_global_historyFragment" app:destination="@id/historyFragment"/>
<action android:id="@+id/action_global_accountProblemFragment" app:destination="@id/accountProblemFragment"/>
<action android:id="@+id/action_global_SitePermissionsManagePhoneFeature" app:destination="@id/SitePermissionsManagePhoneFeature" />
<action android:id="@+id/action_global_collectionCreationFragment" app:destination="@id/collectionCreationFragment" />
<action android:id="@+id/action_global_bookmarkEditFragment" app:destination="@id/bookmarkEditFragment" />
<action android:id="@+id/action_global_addonsManagementFragment" app:destination="@id/addonsManagementFragment" />
<action android:id="@+id/action_global_trackingProtectionFragment" app:destination="@id/trackingProtectionFragment" />
<action android:id="@+id/action_global_exceptionsFragment" app:destination="@id/exceptionsFragment" />
<action android:id="@+id/action_global_accountSettingsFragment" app:destination="@id/accountSettingsFragment" />
<action android:id="@+id/action_global_trackingProtectionPanelDialogFragment" app:destination="@id/trackingProtectionPanelDialogFragment" />
<action android:id="@+id/action_global_quickSettingsSheetDialogFragment" app:destination="@id/quickSettingsSheetDialogFragment"/>
<action android:id="@+id/action_global_tabTrayDialogFragment" app:destination="@id/tabTrayDialogFragment"/>
<action
android:id="@+id/action_global_shareFragment"
app:destination="@id/shareFragment" />
<action
android:id="@+id/action_global_crash_reporter"
app:destination="@id/crashReporterFragment" />
<action
android:id="@+id/action_global_turn_on_sync"
app:destination="@id/turnOnSyncFragment" />
<action
android:id="@+id/action_global_settings_addonsManagementFragment"
app:destination="@id/addonsManagementFragment" />
<action
android:id="@+id/action_global_searchEngineFragment"
app:destination="@id/searchEngineFragment" />
<action
android:id="@+id/action_global_accessibilityFragment"
app:destination="@id/accessibilityFragment" />
<action
android:id="@+id/action_global_deleteBrowsingDataFragment"
app:destination="@id/deleteBrowsingDataFragment" />
<action
android:id="@+id/action_global_webExtensionActionPopupFragment"
app:destination="@id/webExtensionActionPopupFragment" />
<action
android:id="@+id/action_global_settingsFragment"
app:destination="@id/settingsFragment" />
<action
android:id="@+id/action_global_syncedTabsFragment"
app:destination="@+id/syncedTabsFragment" />
<action
android:id="@+id/action_global_privateBrowsingFragment"
app:destination="@id/privateBrowsingFragment" />
<action
android:id="@+id/action_global_bookmarkFragment"
app:destination="@id/bookmarkFragment" />
<action
android:id="@+id/action_global_historyFragment"
app:destination="@id/historyFragment" />
<action
android:id="@+id/action_global_accountProblemFragment"
app:destination="@id/accountProblemFragment" />
<action
android:id="@+id/action_global_SitePermissionsManagePhoneFeature"
app:destination="@id/SitePermissionsManagePhoneFeature" />
<action
android:id="@+id/action_global_collectionCreationFragment"
app:destination="@id/collectionCreationFragment" />
<action
android:id="@+id/action_global_bookmarkEditFragment"
app:destination="@id/bookmarkEditFragment" />
<action
android:id="@+id/action_global_addonsManagementFragment"
app:destination="@id/addonsManagementFragment" />
<action
android:id="@+id/action_global_trackingProtectionFragment"
app:destination="@id/trackingProtectionFragment" />
<action
android:id="@+id/action_global_trackingProtectionExceptionsFragment"
app:destination="@id/trackingProtectionExceptionsFragment" />
<action
android:id="@+id/action_global_accountSettingsFragment"
app:destination="@id/accountSettingsFragment" />
<action
android:id="@+id/action_global_trackingProtectionPanelDialogFragment"
app:destination="@id/trackingProtectionPanelDialogFragment" />
<action
android:id="@+id/action_global_quickSettingsSheetDialogFragment"
app:destination="@id/quickSettingsSheetDialogFragment" />
<action
android:id="@+id/action_global_tabTrayDialogFragment"
app:destination="@id/tabTrayDialogFragment" />
<dialog
android:id="@+id/tabTrayDialogFragment"
android:name="org.mozilla.fenix.tabtray.TabTrayDialogFragment"
tools:layout="@layout/fragment_tab_tray_dialog"/>
tools:layout="@layout/fragment_tab_tray_dialog" />
<fragment
android:id="@+id/homeFragment"
@ -73,9 +121,9 @@
app:argType="boolean" />
<argument
android:name="session_to_delete"
android:defaultValue="@null"
app:argType="string"
app:nullable="true"
android:defaultValue="@null" />
app:nullable="true" />
</fragment>
<fragment
@ -93,8 +141,8 @@
app:nullable="true" />
<argument
android:name="search_access_point"
app:argType="org.mozilla.fenix.components.metrics.Event$PerformedSearch$SearchAccessPoint"
android:defaultValue="NONE" />
android:defaultValue="NONE"
app:argType="org.mozilla.fenix.components.metrics.Event$PerformedSearch$SearchAccessPoint" />
</fragment>
<fragment
@ -149,17 +197,17 @@
tools:layout="@layout/fragment_browser">
<action
android:id="@+id/action_browserFragment_to_searchFragment"
app:destination="@id/searchFragment"
app:enterAnim="@anim/fade_in_up"
app:popExitAnim="@anim/fade_out_down"
app:destination="@id/searchFragment" />
app:popExitAnim="@anim/fade_out_down" />
<argument
android:name="activeSessionId"
app:argType="string"
app:nullable="true" />
<argument
android:name="shouldAnimate"
app:argType="boolean"
android:defaultValue="false" />
android:defaultValue="false"
app:argType="boolean" />
<action
android:id="@+id/action_browserFragment_to_syncedTabsFragment"
app:destination="@id/syncedTabsFragment" />
@ -234,8 +282,8 @@
app:destination="@id/bookmarkSelectFolderFragment" />
<argument
android:name="requiresSnackbarPaddingForToolbar"
app:argType="boolean"
android:defaultValue="false" />
android:defaultValue="false"
app:argType="boolean" />
</fragment>
<fragment
@ -274,25 +322,32 @@
android:label="@string/preferences_passwords_logins_and_passwords">
<action
android:id="@+id/action_savedLoginsAuthFragment_to_loginsListFragment"
app:destination="@id/savedLoginsFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/savedLoginsFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_savedLoginsAuthFragment_to_turnOnSyncFragment"
app:destination="@id/turnOnSyncFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/turnOnSyncFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_savedLoginsAuthFragment_to_savedLoginsSettingFragment"
app:destination="@id/saveLoginSettingFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/saveLoginSettingFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_savedLoginsAuthFragment_to_loginExceptionsFragment"
app:destination="@id/loginExceptionsFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<fragment
@ -313,8 +368,13 @@
android:id="@+id/syncedTabsFragment"
android:name="org.mozilla.fenix.sync.SyncedTabsFragment"
android:label="@string/synced_tabs"
tools:layout="@layout/fragment_synced_tabs">
</fragment>
tools:layout="@layout/fragment_synced_tabs"/>
<fragment
android:id="@+id/loginExceptionsFragment"
android:name="org.mozilla.fenix.loginexceptions.LoginExceptionsFragment"
android:label="@string/preferences_passwords_exceptions"
tools:layout="@layout/fragment_exceptions"/>
<fragment
android:id="@+id/loginDetailFragment"
@ -323,12 +383,12 @@
<argument
android:name="savedLoginId"
app:argType="string"
app:nullable="false"/>
app:nullable="false" />
<action
android:id="@+id/action_loginDetailFragment_to_editLoginFragment"
app:destination="@id/editLoginFragment"
app:popUpTo="@id/editLoginFragment"
app:popUpToInclusive="true"/>
app:popUpToInclusive="true" />
</fragment>
<fragment
@ -338,12 +398,12 @@
<argument
android:name="savedLoginItem"
app:argType="org.mozilla.fenix.settings.logins.SavedLogin"
app:nullable="false"/>
app:nullable="false" />
<action
android:id="@+id/action_editLoginFragment_to_loginDetailFragment"
app:destination="@id/loginDetailFragment"
app:popUpTo="@id/loginDetailFragment"
app:popUpToInclusive="true"/>
app:popUpToInclusive="true" />
</fragment>
<fragment
@ -352,124 +412,124 @@
android:label="@string/settings_title">
<action
android:id="@+id/action_settingsFragment_to_dataChoicesFragment"
app:destination="@id/dataChoicesFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/dataChoicesFragment"
app:popUpTo="@+id/settingsFragment" />
<action
android:id="@+id/action_settingsFragment_to_sitePermissionsFragment"
app:destination="@id/sitePermissionsFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/sitePermissionsFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_savedLoginsAuthFragment"
app:destination="@id/savedLoginsAuthFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/savedLoginsAuthFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_accessibilityFragment"
app:destination="@id/accessibilityFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/accessibilityFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_accountSettingsFragment"
app:destination="@id/accountSettingsFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/accountSettingsFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_searchEngineFragment"
app:destination="@id/searchEngineFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/searchEngineFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_turnOnSyncFragment"
app:destination="@id/turnOnSyncFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/turnOnSyncFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_aboutFragment"
app:destination="@id/aboutFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/aboutFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_secretSettingsFragment"
app:destination="@id/secretSettingsPreference"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/secretSettingsPreference" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_customizationFragment"
app:destination="@id/customizationFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/customizationFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_privateBrowsingFragment"
app:destination="@id/privateBrowsingFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/privateBrowsingFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_trackingProtectionFragment"
app:destination="@id/trackingProtectionFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/trackingProtectionFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_deleteBrowsingDataFragment"
app:destination="@id/deleteBrowsingDataFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/deleteBrowsingDataFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_accountProblemFragment"
app:destination="@id/accountProblemFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/accountProblemFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_deleteBrowsingDataOnQuitFragment"
app:destination="@id/deleteBrowsingDataOnQuitFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/deleteBrowsingDataOnQuitFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_localeSettingsFragment"
app:destination="@id/localeSettingsFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/localeSettingsFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_settingsFragment_to_addonsFragment"
app:destination="@id/addonsManagementFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/addonsManagementFragment" />
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/dataChoicesFragment"
@ -481,19 +541,19 @@
android:label="@string/preferences_site_permissions">
<action
android:id="@+id/action_site_permissions_to_manage_phone_features"
app:destination="@id/SitePermissionsManagePhoneFeature"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/SitePermissionsManagePhoneFeature"
app:popUpTo="@id/sitePermissionsFragment" />
<action
android:id="@+id/action_site_permissions_to_exceptions"
app:destination="@id/sitePermissionsExceptionsFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/sitePermissionsExceptionsFragment"
app:popUpTo="@id/sitePermissionsFragment" />
</fragment>
@ -541,7 +601,7 @@
<fragment
android:id="@+id/aboutFragment"
android:name="org.mozilla.fenix.settings.about.AboutFragment"/>
android:name="org.mozilla.fenix.settings.about.AboutFragment" />
<fragment
android:id="@+id/secretSettingsPreference"
android:name="org.mozilla.fenix.settings.SecretSettingsFragment"
@ -566,27 +626,27 @@
android:name="org.mozilla.fenix.settings.TrackingProtectionFragment">
<action
android:id="@+id/action_trackingProtectionFragment_to_exceptionsFragment"
app:destination="@id/trackingProtectionExceptionsFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/exceptionsFragment" />
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_trackingProtectionFragment_to_trackingProtectionBlockingFragment"
app:destination="@id/trackingProtectionBlockingFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:destination="@id/trackingProtectionBlockingFragment" />
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/deleteBrowsingDataFragment"
android:name="org.mozilla.fenix.settings.deletebrowsingdata.DeleteBrowsingDataFragment"
android:label="@string/preferences_delete_browsing_data" />
<fragment
android:id="@+id/exceptionsFragment"
android:name="org.mozilla.fenix.exceptions.ExceptionsFragment"
android:label="@string/preference_exceptions"/>
android:id="@+id/trackingProtectionExceptionsFragment"
android:name="org.mozilla.fenix.trackingprotectionexceptions.TrackingProtectionExceptionsFragment"
android:label="@string/preference_exceptions" />
<dialog
android:id="@+id/collectionCreationFragment"
android:name="org.mozilla.fenix.collections.CollectionCreationFragment"
@ -795,6 +855,6 @@
<argument
android:name="webExtensionTitle"
app:argType="string"
app:nullable="true"/>
app:nullable="true" />
</fragment>
</navigation>

View File

@ -185,4 +185,6 @@
<string name="pref_key_search_widget_cfr_manually_dismissed" translatable="false">pref_key_search_widget_cfr_manually_dismissed</string>
<string name="pref_key_is_in_search_widget_experiment" translatable="false">pref_key_is_in_search_widget_experiment</string>
<string name="pref_key_show_search_widget_cfr" translatable="false">pref_key_show_search_widget_cfr</string>
<string name="pref_key_login_exceptions" translatable="false">pref_key_login_exceptions</string>
</resources>

View File

@ -1217,6 +1217,8 @@
<string name="preferences_passwords_exceptions_description_empty">Logins and passwords that are not saved will be shown here.</string>
<!-- Description of list of login exceptions that we never save logins for -->
<string name="preferences_passwords_exceptions_description">Logins and passwords will not be saved for these sites.</string>
<!-- Text on button to remove all saved login exceptions -->
<string name="preferences_passwords_exceptions_remove_all">Delete all exceptions</string>
<!-- Hint for search box in logins list -->
<string name="preferences_passwords_saved_logins_search">Search logins</string>
<!-- Option to sort logins list A-Z, alphabetically -->

View File

@ -2,7 +2,8 @@
<!-- 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/. -->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
android:key="@string/pref_key_save_logins_settings"
android:summary="@string/preferences_passwords_save_logins_ask_to_save"
@ -18,4 +19,9 @@
<Preference
android:key="@string/pref_key_saved_logins"
android:title="@string/preferences_passwords_saved_logins" />
<androidx.preference.Preference
android:key="@string/pref_key_login_exceptions"
android:title="@string/preferences_passwords_exceptions"
app:icon="@drawable/ic_internet" />
</PreferenceScreen>

View File

@ -2,19 +2,21 @@
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.exceptions
package org.mozilla.fenix.loginexceptions
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import mozilla.components.feature.logins.exceptions.LoginException
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
import org.junit.Test
class ExceptionsFragmentStoreTest {
class LoginExceptionFragmentStoreTest {
@Test
fun onChange() = runBlocking {
val initialState = emptyDefaultState()
val store = ExceptionsFragmentStore(initialState)
val newExceptionsItem = ExceptionItem("URL")
val newExceptionsItem: LoginException = mockk()
store.dispatch(ExceptionsFragmentAction.Change(listOf(newExceptionsItem))).join()
assertNotSame(initialState, store.state)

View File

@ -0,0 +1,36 @@
/* 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 io.mockk.mockk
import mozilla.components.feature.logins.exceptions.LoginException
import org.junit.Assert.assertEquals
import org.junit.Test
class LoginExceptionsInteractorTest {
@Test
fun onDeleteAll() {
var onDeleteAll = false
val interactor = LoginExceptionsInteractor(
mockk(),
{ onDeleteAll = true }
)
interactor.onDeleteAll()
assertEquals(true, onDeleteAll)
}
@Test
fun onDeleteOne() {
var exceptionsItemReceived: LoginException? = null
val exceptionsItem: LoginException = mockk()
val interactor = LoginExceptionsInteractor(
{ exceptionsItemReceived = exceptionsItem },
mockk()
)
interactor.onDeleteOne(exceptionsItem)
assertEquals(exceptionsItemReceived, exceptionsItem)
}
}

View File

@ -2,7 +2,7 @@
* 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.exceptions
package org.mozilla.fenix.trackingprotectionexceptions
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -11,10 +11,10 @@ import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.exceptions.viewholders.ExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsHeaderViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsListItemViewHolder
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsHeaderViewHolder
import org.mozilla.fenix.trackingprotectionexceptions.viewholders.ExceptionsListItemViewHolder
@ExperimentalCoroutinesApi
@RunWith(FenixRobolectricTestRunner::class)

View File

@ -2,7 +2,7 @@
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.exceptions
package org.mozilla.fenix.trackingprotectionexceptions
import io.mockk.mockk
import mozilla.components.concept.engine.content.blocking.TrackingProtectionException
@ -14,11 +14,12 @@ class ExceptionsInteractorTest {
@Test
fun onLearnMore() {
var learnMoreClicked = false
val interactor = ExceptionsInteractor(
{ learnMoreClicked = true },
mockk(),
mockk()
)
val interactor =
ExceptionsInteractor(
{ learnMoreClicked = true },
mockk(),
mockk()
)
interactor.onLearnMore()
assertEquals(true, learnMoreClicked)
}
@ -26,11 +27,12 @@ class ExceptionsInteractorTest {
@Test
fun onDeleteAll() {
var onDeleteAll = false
val interactor = ExceptionsInteractor(
mockk(),
mockk(),
{ onDeleteAll = true }
)
val interactor =
ExceptionsInteractor(
mockk(),
mockk(),
{ onDeleteAll = true }
)
interactor.onDeleteAll()
assertEquals(true, onDeleteAll)
}
@ -38,12 +40,14 @@ class ExceptionsInteractorTest {
@Test
fun onDeleteOne() {
var exceptionsItemReceived: TrackingProtectionException? = null
val exceptionsItem = ExceptionItem("url")
val interactor = ExceptionsInteractor(
mockk(),
{ exceptionsItemReceived = exceptionsItem },
mockk()
)
val exceptionsItem =
ExceptionItem("url")
val interactor =
ExceptionsInteractor(
mockk(),
{ exceptionsItemReceived = exceptionsItem },
mockk()
)
interactor.onDeleteOne(exceptionsItem)
assertEquals(exceptionsItemReceived, exceptionsItem)
}

View File

@ -2,7 +2,7 @@
* 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.exceptions
package org.mozilla.fenix.trackingprotectionexceptions
import android.text.Spannable
import android.text.method.LinkMovementMethod

View File

@ -0,0 +1,35 @@
/* 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.trackingprotectionexceptions
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
import org.junit.Test
class TrackingProtectionExceptionsFragmentStoreTest {
@Test
fun onChange() = runBlocking {
val initialState = emptyDefaultState()
val store =
ExceptionsFragmentStore(
initialState
)
val newExceptionsItem =
ExceptionItem("URL")
store.dispatch(ExceptionsFragmentAction.Change(listOf(newExceptionsItem))).join()
assertNotSame(initialState, store.state)
assertEquals(
store.state.items,
listOf(newExceptionsItem)
)
}
private fun emptyDefaultState(): ExceptionsFragmentState =
ExceptionsFragmentState(
items = listOf()
)
}

View File

@ -2,7 +2,7 @@
* 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.exceptions.viewholders
package org.mozilla.fenix.trackingprotectionexceptions.viewholders
import android.view.LayoutInflater
import android.view.View
@ -15,7 +15,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.exceptions.ExceptionsInteractor
import org.mozilla.fenix.trackingprotectionexceptions.ExceptionsInteractor
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)

View File

@ -2,7 +2,7 @@
* 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.exceptions.viewholders
package org.mozilla.fenix.trackingprotectionexceptions.viewholders
import android.view.LayoutInflater
import android.view.View
@ -15,7 +15,7 @@ import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.exceptions.ExceptionsInteractor
import org.mozilla.fenix.trackingprotectionexceptions.ExceptionsInteractor
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)

View File

@ -106,6 +106,7 @@ object Deps {
const val mozilla_feature_pwa = "org.mozilla.components:feature-pwa:${Versions.mozilla_android_components}"
const val mozilla_feature_toolbar = "org.mozilla.components:feature-toolbar:${Versions.mozilla_android_components}"
const val mozilla_feature_findinpage = "org.mozilla.components:feature-findinpage:${Versions.mozilla_android_components}"
const val mozilla_feature_logins = "org.mozilla.components:feature-logins:${Versions.mozilla_android_components}"
const val mozilla_feature_site_permissions = "org.mozilla.components:feature-sitepermissions:${Versions.mozilla_android_components}"
const val mozilla_feature_readerview = "org.mozilla.components:feature-readerview:${Versions.mozilla_android_components}"
const val mozilla_feature_tab_collections = "org.mozilla.components:feature-tab-collections:${Versions.mozilla_android_components}"