Copione merged onto master
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
commit
45aabf76c9
|
@ -2444,6 +2444,39 @@ logins:
|
|||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
expires: "2020-10-01"
|
||||
open_login_editor:
|
||||
type: event
|
||||
description: |
|
||||
A user entered the edit screen for an individual saved login
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/10173
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/11208
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
expires: "2020-10-01"
|
||||
delete_saved_login:
|
||||
type: event
|
||||
description: |
|
||||
A user confirms delete of a saved login
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/10173
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/11208
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
expires: "2020-10-01"
|
||||
save_edited_login:
|
||||
type: event
|
||||
description: |
|
||||
A user saves changes made to an individual login
|
||||
bugs:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/10173
|
||||
data_reviews:
|
||||
- https://github.com/mozilla-mobile/fenix/issues/11208
|
||||
notification_emails:
|
||||
- fenix-core@mozilla.com
|
||||
expires: "2020-10-01"
|
||||
|
||||
download_notification:
|
||||
resume:
|
||||
|
|
|
@ -159,6 +159,7 @@ class HistoryTest {
|
|||
}.openHistory {
|
||||
}.openThreeDotMenu {
|
||||
}.clickDelete {
|
||||
verifyDeleteSnackbarText("Deleted")
|
||||
verifyEmptyHistoryView()
|
||||
}
|
||||
}
|
||||
|
@ -175,6 +176,7 @@ class HistoryTest {
|
|||
clickDeleteHistoryButton()
|
||||
verifyDeleteConfirmationMessage()
|
||||
confirmDeleteAllHistory()
|
||||
verifyDeleteSnackbarText("Browsing data deleted")
|
||||
verifyEmptyHistoryView()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,6 +82,8 @@ class HistoryRobot {
|
|||
.click()
|
||||
}
|
||||
|
||||
fun verifyDeleteSnackbarText(text: String) = assertSnackBarText(text)
|
||||
|
||||
class Transition {
|
||||
fun closeMenu(interact: HistoryRobot.() -> Unit): Transition {
|
||||
closeButton().click()
|
||||
|
@ -158,3 +160,6 @@ private fun assertDeleteConfirmationMessage() =
|
|||
.check(matches(isDisplayed()))
|
||||
|
||||
private fun assertCopySnackBarText() = snackBarText().check(matches(withText("URL copied")))
|
||||
|
||||
private fun assertSnackBarText(text: String) =
|
||||
snackBarText().check(matches(withText(Matchers.containsString(text))))
|
||||
|
|
|
@ -26,6 +26,7 @@ import androidx.navigation.fragment.NavHostFragment
|
|||
import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.NavigationUI
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.gms.tasks.Tasks.call
|
||||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -86,8 +87,8 @@ import org.mozilla.fenix.session.NotificationSessionObserver
|
|||
import org.mozilla.fenix.settings.SettingsFragmentDirections
|
||||
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
|
||||
import org.mozilla.fenix.settings.about.AboutFragmentDirections
|
||||
import org.mozilla.fenix.settings.logins.LoginDetailFragmentDirections
|
||||
import org.mozilla.fenix.settings.logins.SavedLoginsAuthFragmentDirections
|
||||
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections
|
||||
import org.mozilla.fenix.settings.logins.fragment.LoginDetailFragmentDirections
|
||||
import org.mozilla.fenix.settings.search.AddSearchEngineFragmentDirections
|
||||
import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections
|
||||
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
|
||||
|
|
|
@ -8,13 +8,13 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Switch
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import kotlinx.android.synthetic.main.fragment_installed_add_on_details.view.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -301,7 +301,7 @@ class InstalledAddonDetailsFragment : Fragment() {
|
|||
view.remove_add_on.isClickable = clickable
|
||||
}
|
||||
|
||||
private fun Switch.setState(checked: Boolean) {
|
||||
private fun SwitchMaterial.setState(checked: Boolean) {
|
||||
val text = if (checked) {
|
||||
R.string.mozac_feature_addons_enabled
|
||||
} else {
|
||||
|
|
|
@ -40,7 +40,7 @@ import org.mozilla.fenix.ext.navigateSafe
|
|||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.resetPoliciesAfter
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.shortcut.FirstTimePwaObserver
|
||||
import org.mozilla.fenix.shortcut.PwaOnboardingObserver
|
||||
import org.mozilla.fenix.trackingprotection.TrackingProtectionOverlay
|
||||
|
||||
/**
|
||||
|
@ -156,9 +156,9 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
|
|||
}
|
||||
session?.register(toolbarSessionObserver, viewLifecycleOwner, autoPause = true)
|
||||
|
||||
if (settings.shouldShowFirstTimePwaFragment) {
|
||||
if (!settings.userKnowsAboutPwas) {
|
||||
session?.register(
|
||||
FirstTimePwaObserver(
|
||||
PwaOnboardingObserver(
|
||||
navController = findNavController(),
|
||||
settings = settings,
|
||||
webAppUseCases = context.components.useCases.webAppUseCases
|
||||
|
|
|
@ -11,22 +11,24 @@ import androidx.core.view.isGone
|
|||
import androidx.core.view.isInvisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.collection_tab_list_row.view.*
|
||||
import kotlinx.android.synthetic.main.collection_tab_list_row.*
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.loadIntoView
|
||||
import org.mozilla.fenix.home.Tab
|
||||
import org.mozilla.fenix.utils.view.ViewHolder
|
||||
|
||||
class CollectionCreationTabListAdapter(
|
||||
private val interactor: CollectionCreationInteractor
|
||||
) : RecyclerView.Adapter<TabViewHolder>() {
|
||||
|
||||
private var tabs: List<Tab> = listOf()
|
||||
private var selectedTabs: MutableSet<Tab> = mutableSetOf()
|
||||
private var hideCheckboxes = false
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabViewHolder {
|
||||
val view =
|
||||
LayoutInflater.from(parent.context).inflate(TabViewHolder.LAYOUT_ID, parent, false)
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(TabViewHolder.LAYOUT_ID, parent, false)
|
||||
|
||||
return TabViewHolder(view)
|
||||
}
|
||||
|
@ -39,11 +41,11 @@ class CollectionCreationTabListAdapter(
|
|||
is CheckChanged -> {
|
||||
val checkChanged = payloads[0] as CheckChanged
|
||||
if (checkChanged.shouldBeChecked) {
|
||||
holder.itemView.tab_selected_checkbox.isChecked = true
|
||||
holder.tab_selected_checkbox.isChecked = true
|
||||
} else if (checkChanged.shouldBeUnchecked) {
|
||||
holder.itemView.tab_selected_checkbox.isChecked = false
|
||||
holder.tab_selected_checkbox.isChecked = false
|
||||
}
|
||||
holder.itemView.tab_selected_checkbox.isGone = checkChanged.shouldHideCheckBox
|
||||
holder.tab_selected_checkbox.isGone = checkChanged.shouldHideCheckBox
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +54,7 @@ class CollectionCreationTabListAdapter(
|
|||
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
|
||||
val tab = tabs[position]
|
||||
val isSelected = selectedTabs.contains(tab)
|
||||
holder.itemView.tab_selected_checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||
holder.tab_selected_checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
selectedTabs.add(tab)
|
||||
interactor.addTabToSelection(tab)
|
||||
|
@ -86,57 +88,24 @@ class CollectionCreationTabListAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
private class TabDiffUtil(
|
||||
val old: List<Tab>,
|
||||
val new: List<Tab>,
|
||||
val oldSelected: Set<Tab>,
|
||||
val newSelected: Set<Tab>,
|
||||
val oldHideCheckboxes: Boolean,
|
||||
val newHideCheckboxes: Boolean
|
||||
) : DiffUtil.Callback() {
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
old[oldItemPosition].sessionId == new[newItemPosition].sessionId
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val isSameTab = old[oldItemPosition].sessionId == new[newItemPosition].sessionId
|
||||
val sameSelectedState = oldSelected.contains(old[oldItemPosition]) == newSelected.contains(new[newItemPosition])
|
||||
val isSameHideCheckboxes = oldHideCheckboxes == newHideCheckboxes
|
||||
return isSameTab && sameSelectedState && isSameHideCheckboxes
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
|
||||
val shouldBeChecked = newSelected.contains(new[newItemPosition]) && !oldSelected.contains(old[oldItemPosition])
|
||||
val shouldBeUnchecked =
|
||||
!newSelected.contains(new[newItemPosition]) && oldSelected.contains(old[oldItemPosition])
|
||||
return CheckChanged(shouldBeChecked, shouldBeUnchecked, newHideCheckboxes)
|
||||
}
|
||||
|
||||
override fun getOldListSize(): Int = old.size
|
||||
override fun getNewListSize(): Int = new.size
|
||||
}
|
||||
|
||||
data class CheckChanged(val shouldBeChecked: Boolean, val shouldBeUnchecked: Boolean, val shouldHideCheckBox: Boolean)
|
||||
|
||||
class TabViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
private val checkbox = view.tab_selected_checkbox!!
|
||||
class TabViewHolder(view: View) : ViewHolder(view) {
|
||||
|
||||
init {
|
||||
view.collection_item_tab.setOnClickListener {
|
||||
checkbox.isChecked = !checkbox.isChecked
|
||||
collection_item_tab.setOnClickListener {
|
||||
tab_selected_checkbox.isChecked = !tab_selected_checkbox.isChecked
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(tab: Tab, isSelected: Boolean, shouldHideCheckBox: Boolean) {
|
||||
itemView.hostname.text = tab.hostname
|
||||
itemView.tab_title.text = tab.title
|
||||
checkbox.isInvisible = shouldHideCheckBox
|
||||
hostname.text = tab.hostname
|
||||
tab_title.text = tab.title
|
||||
tab_selected_checkbox.isInvisible = shouldHideCheckBox
|
||||
itemView.isClickable = !shouldHideCheckBox
|
||||
if (checkbox.isChecked != isSelected) {
|
||||
checkbox.isChecked = isSelected
|
||||
if (tab_selected_checkbox.isChecked != isSelected) {
|
||||
tab_selected_checkbox.isChecked = isSelected
|
||||
}
|
||||
|
||||
itemView.context.components.core.icons.loadIntoView(itemView.favicon_image, tab.url)
|
||||
itemView.context.components.core.icons.loadIntoView(favicon_image, tab.url)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -10,12 +10,13 @@ import android.view.ViewGroup
|
|||
import androidx.core.graphics.BlendModeColorFilterCompat.createBlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat.SRC_IN
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.collections_list_item.view.*
|
||||
import kotlinx.android.synthetic.main.collections_list_item.*
|
||||
import mozilla.components.feature.tab.collections.TabCollection
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.description
|
||||
import org.mozilla.fenix.ext.getIconColor
|
||||
import org.mozilla.fenix.home.Tab
|
||||
import org.mozilla.fenix.utils.view.ViewHolder
|
||||
|
||||
class SaveCollectionListAdapter(
|
||||
private val interactor: CollectionCreationInteractor
|
||||
|
@ -48,12 +49,12 @@ class SaveCollectionListAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
class CollectionViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
class CollectionViewHolder(view: View) : ViewHolder(view) {
|
||||
|
||||
fun bind(collection: TabCollection) {
|
||||
itemView.collection_item.text = collection.title
|
||||
itemView.collection_description.text = collection.description(itemView.context)
|
||||
itemView.collection_icon.colorFilter =
|
||||
collection_item.text = collection.title
|
||||
collection_description.text = collection.description(itemView.context)
|
||||
collection_icon.colorFilter =
|
||||
createBlendModeColorFilterCompat(collection.getIconColor(itemView.context), SRC_IN)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/* 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.collections
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import org.mozilla.fenix.home.Tab
|
||||
|
||||
/**
|
||||
* Diff callback for comparing tab lists with selected state.
|
||||
*/
|
||||
internal class TabDiffUtil(
|
||||
private val old: List<Tab>,
|
||||
private val new: List<Tab>,
|
||||
private val oldSelected: Set<Tab>,
|
||||
private val newSelected: Set<Tab>,
|
||||
private val oldHideCheckboxes: Boolean,
|
||||
private val newHideCheckboxes: Boolean
|
||||
) : DiffUtil.Callback() {
|
||||
|
||||
/**
|
||||
* Checks if the tabs in the given positions refer to the same tab (based on ID).
|
||||
*/
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
old[oldItemPosition].sessionId == new[newItemPosition].sessionId
|
||||
|
||||
/**
|
||||
* Checks if the combination of tab ID, selection, and checkbox visibility is the same.
|
||||
*/
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val isSameTab = old[oldItemPosition].sessionId == new[newItemPosition].sessionId
|
||||
val sameSelectedState = oldItemSelected(oldItemPosition) == newItemSelected(newItemPosition)
|
||||
val isSameHideCheckboxes = oldHideCheckboxes == newHideCheckboxes
|
||||
return isSameTab && sameSelectedState && isSameHideCheckboxes
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a change payload indication if the item is now/no longer selected.
|
||||
*/
|
||||
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
|
||||
val shouldBeChecked = newItemSelected(newItemPosition) && !oldItemSelected(oldItemPosition)
|
||||
val shouldBeUnchecked = !newItemSelected(newItemPosition) && oldItemSelected(oldItemPosition)
|
||||
return CheckChanged(shouldBeChecked, shouldBeUnchecked, newHideCheckboxes)
|
||||
}
|
||||
|
||||
override fun getOldListSize(): Int = old.size
|
||||
override fun getNewListSize(): Int = new.size
|
||||
|
||||
private fun oldItemSelected(oldItemPosition: Int) = oldSelected.contains(old[oldItemPosition])
|
||||
private fun newItemSelected(newItemPosition: Int) = newSelected.contains(new[newItemPosition])
|
||||
}
|
||||
|
||||
/**
|
||||
* @property shouldBeChecked Item was previously unchecked and should be checked.
|
||||
* @property shouldBeUnchecked Item was previously checked and should be unchecked.
|
||||
* @property shouldHideCheckBox Checkbox should be visible.
|
||||
*/
|
||||
data class CheckChanged(
|
||||
val shouldBeChecked: Boolean,
|
||||
val shouldBeUnchecked: Boolean,
|
||||
val shouldHideCheckBox: Boolean
|
||||
)
|
|
@ -63,7 +63,7 @@ class UseCases(
|
|||
|
||||
val downloadUseCases by lazy { DownloadsUseCases(store) }
|
||||
|
||||
val contextMenuUseCases by lazy { ContextMenuUseCases(sessionManager, store) }
|
||||
val contextMenuUseCases by lazy { ContextMenuUseCases(store) }
|
||||
|
||||
val engineSessionUseCases by lazy { EngineSessionUseCases(sessionManager) }
|
||||
|
||||
|
|
|
@ -466,6 +466,15 @@ private val Event.wrapper: EventWrapper<*>?
|
|||
is Event.ViewLoginPassword -> EventWrapper<NoExtraKeys>(
|
||||
{ Logins.viewPasswordLogin.record(it) }
|
||||
)
|
||||
is Event.DeleteLogin -> EventWrapper<NoExtraKeys>(
|
||||
{ Logins.deleteSavedLogin.record(it) }
|
||||
)
|
||||
is Event.EditLogin -> EventWrapper<NoExtraKeys>(
|
||||
{ Logins.openLoginEditor.record(it) }
|
||||
)
|
||||
is Event.EditLoginSave -> EventWrapper<NoExtraKeys>(
|
||||
{ Logins.saveEditedLogin.record(it) }
|
||||
)
|
||||
is Event.PrivateBrowsingShowSearchSuggestions -> EventWrapper<NoExtraKeys>(
|
||||
{ SearchSuggestions.enableInPrivate.record(it) }
|
||||
)
|
||||
|
|
|
@ -154,6 +154,9 @@ sealed class Event {
|
|||
object OpenLogins : Event()
|
||||
object OpenOneLogin : Event()
|
||||
object CopyLogin : Event()
|
||||
object DeleteLogin : Event()
|
||||
object EditLogin : Event()
|
||||
object EditLoginSave : Event()
|
||||
object ViewLoginPassword : Event()
|
||||
object CustomEngineAdded : Event()
|
||||
object CustomEngineDeleted : Event()
|
||||
|
|
|
@ -98,7 +98,6 @@ import org.mozilla.fenix.home.sessioncontrol.SessionControlView
|
|||
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
|
||||
import org.mozilla.fenix.onboarding.FenixOnboarding
|
||||
import org.mozilla.fenix.settings.SupportUtils
|
||||
import org.mozilla.fenix.settings.SupportUtils.MozillaPage.PRIVATE_NOTICE
|
||||
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
|
||||
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
|
||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
||||
|
@ -175,6 +174,7 @@ class HomeFragment : Fragment() {
|
|||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_home, container, false)
|
||||
val activity = activity as HomeActivity
|
||||
val components = requireComponents
|
||||
|
||||
currentMode = CurrentMode(
|
||||
view.context,
|
||||
|
@ -186,11 +186,11 @@ class HomeFragment : Fragment() {
|
|||
homeFragmentStore = StoreProvider.get(this) {
|
||||
HomeFragmentStore(
|
||||
HomeFragmentState(
|
||||
collections = requireComponents.core.tabCollectionStorage.cachedTabCollections,
|
||||
collections = components.core.tabCollectionStorage.cachedTabCollections,
|
||||
expandedCollections = emptySet(),
|
||||
mode = currentMode.getCurrentMode(),
|
||||
topSites = StrictMode.allowThreadDiskReads().resetPoliciesAfter {
|
||||
requireComponents.core.topSiteStorage.cachedTopSites
|
||||
components.core.topSiteStorage.cachedTopSites
|
||||
},
|
||||
tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip()
|
||||
)
|
||||
|
@ -200,16 +200,18 @@ class HomeFragment : Fragment() {
|
|||
_sessionControlInteractor = SessionControlInteractor(
|
||||
DefaultSessionControlController(
|
||||
activity = activity,
|
||||
engine = components.core.engine,
|
||||
metrics = components.analytics.metrics,
|
||||
sessionManager = sessionManager,
|
||||
tabCollectionStorage = components.core.tabCollectionStorage,
|
||||
topSiteStorage = components.core.topSiteStorage,
|
||||
addTabUseCase = components.useCases.tabsUseCases.addTab,
|
||||
fragmentStore = homeFragmentStore,
|
||||
navController = findNavController(),
|
||||
viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
|
||||
getListOfTabs = ::getListOfTabs,
|
||||
hideOnboarding = ::hideOnboardingAndOpenSearch,
|
||||
registerCollectionStorageObserver = ::registerCollectionStorageObserver,
|
||||
showDeleteCollectionPrompt = ::showDeleteCollectionPrompt,
|
||||
openSettingsScreen = ::openSettingsScreen,
|
||||
openWhatsNewLink = { openInNormalTab(SupportUtils.getWhatsNewUrl(activity)) },
|
||||
openPrivacyNotice = { openInNormalTab(SupportUtils.getMozillaPageUrl(PRIVATE_NOTICE)) },
|
||||
showTabTray = ::openTabTray
|
||||
)
|
||||
)
|
||||
|
@ -611,11 +613,6 @@ class HomeFragment : Fragment() {
|
|||
nav(R.id.homeFragment, directions, getToolbarNavOptions(requireContext()))
|
||||
}
|
||||
|
||||
private fun openSettingsScreen() {
|
||||
val directions = HomeFragmentDirections.actionGlobalPrivateBrowsingFragment()
|
||||
nav(R.id.homeFragment, directions)
|
||||
}
|
||||
|
||||
private fun openInNormalTab(url: String) {
|
||||
(activity as HomeActivity).openToBrowserAndLoad(
|
||||
searchTermOrURL = url,
|
||||
|
@ -767,13 +764,8 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getListOfSessions(private: Boolean = browsingModeManager.mode.isPrivate): List<Session> {
|
||||
return sessionManager.sessionsOfType(private = private)
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun getListOfTabs(): List<Tab> {
|
||||
return getListOfSessions().toTabs()
|
||||
private fun getNumberOfSessions(private: Boolean = browsingModeManager.mode.isPrivate): Int {
|
||||
return sessionManager.sessionsOfType(private = private).count()
|
||||
}
|
||||
|
||||
private fun registerCollectionStorageObserver() {
|
||||
|
@ -787,7 +779,7 @@ class HomeFragment : Fragment() {
|
|||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val recyclerView = sessionControlView!!.view
|
||||
delay(ANIM_SCROLL_DELAY)
|
||||
val tabsSize = getListOfSessions().size
|
||||
val tabsSize = getNumberOfSessions()
|
||||
|
||||
var indexOfCollection = tabsSize + NON_TAB_ITEM_NUM
|
||||
changedCollection?.let { changedCollection ->
|
||||
|
|
|
@ -9,9 +9,11 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.browser.session.SessionManager
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.concept.engine.prompt.ShareData
|
||||
import mozilla.components.feature.tab.collections.TabCollection
|
||||
import mozilla.components.feature.tab.collections.ext.restore
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import mozilla.components.feature.top.sites.TopSite
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
|
@ -23,13 +25,12 @@ import org.mozilla.fenix.components.TopSiteStorage
|
|||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.components.tips.Tip
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.nav
|
||||
import org.mozilla.fenix.ext.sessionsOfType
|
||||
import org.mozilla.fenix.home.HomeFragment
|
||||
import org.mozilla.fenix.home.HomeFragmentAction
|
||||
import org.mozilla.fenix.home.HomeFragmentDirections
|
||||
import org.mozilla.fenix.home.HomeFragmentStore
|
||||
import org.mozilla.fenix.home.Tab
|
||||
import org.mozilla.fenix.settings.SupportUtils
|
||||
import mozilla.components.feature.tab.collections.Tab as ComponentTab
|
||||
|
||||
|
@ -130,26 +131,20 @@ interface SessionControlController {
|
|||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||
class DefaultSessionControlController(
|
||||
private val activity: HomeActivity,
|
||||
private val engine: Engine,
|
||||
private val metrics: MetricController,
|
||||
private val sessionManager: SessionManager,
|
||||
private val tabCollectionStorage: TabCollectionStorage,
|
||||
private val topSiteStorage: TopSiteStorage,
|
||||
private val addTabUseCase: TabsUseCases.AddNewTabUseCase,
|
||||
private val fragmentStore: HomeFragmentStore,
|
||||
private val navController: NavController,
|
||||
private val viewLifecycleScope: CoroutineScope,
|
||||
private val getListOfTabs: () -> List<Tab>,
|
||||
private val hideOnboarding: () -> Unit,
|
||||
private val registerCollectionStorageObserver: () -> Unit,
|
||||
private val showDeleteCollectionPrompt: (tabCollection: TabCollection, title: String?, message: String) -> Unit,
|
||||
private val openSettingsScreen: () -> Unit,
|
||||
private val openWhatsNewLink: () -> Unit,
|
||||
private val openPrivacyNotice: () -> Unit,
|
||||
private val showTabTray: () -> Unit
|
||||
) : SessionControlController {
|
||||
private val metrics: MetricController
|
||||
get() = activity.components.analytics.metrics
|
||||
private val sessionManager: SessionManager
|
||||
get() = activity.components.core.sessionManager
|
||||
private val tabCollectionStorage: TabCollectionStorage
|
||||
get() = activity.components.core.tabCollectionStorage
|
||||
private val topSiteStorage: TopSiteStorage
|
||||
get() = activity.components.core.topSiteStorage
|
||||
|
||||
override fun handleCollectionAddTabTapped(collection: TabCollection) {
|
||||
metrics.track(Event.CollectionAddTabPressed)
|
||||
|
@ -162,7 +157,7 @@ class DefaultSessionControlController(
|
|||
override fun handleCollectionOpenTabClicked(tab: ComponentTab) {
|
||||
sessionManager.restore(
|
||||
activity,
|
||||
activity.components.core.engine,
|
||||
engine,
|
||||
tab,
|
||||
onTabRestored = {
|
||||
activity.openToBrowser(BrowserDirection.FromHome)
|
||||
|
@ -182,10 +177,10 @@ class DefaultSessionControlController(
|
|||
override fun handleCollectionOpenTabsTapped(collection: TabCollection) {
|
||||
sessionManager.restore(
|
||||
activity,
|
||||
activity.components.core.engine,
|
||||
engine,
|
||||
collection,
|
||||
onFailure = { url ->
|
||||
activity.components.useCases.tabsUseCases.addTab.invoke(url)
|
||||
addTabUseCase.invoke(url)
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -261,7 +256,7 @@ class DefaultSessionControlController(
|
|||
metrics.track(Event.TopSiteOpenInNewTab)
|
||||
if (isDefault) { metrics.track(Event.TopSiteOpenDefault) }
|
||||
if (url == SupportUtils.POCKET_TRENDING_URL) { metrics.track(Event.PocketTopSiteClicked) }
|
||||
activity.components.useCases.tabsUseCases.addTab.invoke(
|
||||
addTabUseCase.invoke(
|
||||
url = url,
|
||||
selectTab = true,
|
||||
startLoading = true
|
||||
|
@ -274,15 +269,24 @@ class DefaultSessionControlController(
|
|||
}
|
||||
|
||||
override fun handleOpenSettingsClicked() {
|
||||
openSettingsScreen()
|
||||
val directions = HomeFragmentDirections.actionGlobalPrivateBrowsingFragment()
|
||||
navController.nav(R.id.homeFragment, directions)
|
||||
}
|
||||
|
||||
override fun handleWhatsNewGetAnswersClicked() {
|
||||
openWhatsNewLink()
|
||||
activity.openToBrowserAndLoad(
|
||||
searchTermOrURL = SupportUtils.getWhatsNewUrl(activity),
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromHome
|
||||
)
|
||||
}
|
||||
|
||||
override fun handleReadPrivacyNoticeClicked() {
|
||||
openPrivacyNotice()
|
||||
activity.openToBrowserAndLoad(
|
||||
searchTermOrURL = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromHome
|
||||
)
|
||||
}
|
||||
|
||||
override fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean) {
|
||||
|
@ -303,7 +307,11 @@ class DefaultSessionControlController(
|
|||
// Only register the observer right before moving to collection creation
|
||||
registerCollectionStorageObserver()
|
||||
|
||||
val tabIds = getListOfTabs().map { it.sessionId }.toTypedArray()
|
||||
val tabIds = sessionManager
|
||||
.sessionsOfType(private = activity.browsingModeManager.mode.isPrivate)
|
||||
.map { session -> session.id }
|
||||
.toList()
|
||||
.toTypedArray()
|
||||
val directions = HomeFragmentDirections.actionGlobalCollectionCreationFragment(
|
||||
tabIds = tabIds,
|
||||
saveCollectionStep = step,
|
||||
|
|
|
@ -42,7 +42,7 @@ interface BookmarkController {
|
|||
fun handleBookmarkSharing(item: BookmarkNode)
|
||||
fun handleOpeningBookmark(item: BookmarkNode, mode: BrowsingMode)
|
||||
fun handleBookmarkDeletion(nodes: Set<BookmarkNode>, eventType: Event)
|
||||
fun handleBookmarkFolderDeletion(node: BookmarkNode)
|
||||
fun handleBookmarkFolderDeletion(nodes: Set<BookmarkNode>)
|
||||
fun handleRequestSync()
|
||||
fun handleBackPressed()
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ class DefaultBookmarkController(
|
|||
private val loadBookmarkNode: suspend (String) -> BookmarkNode?,
|
||||
private val showSnackbar: (String) -> Unit,
|
||||
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit,
|
||||
private val deleteBookmarkFolder: (BookmarkNode) -> Unit,
|
||||
private val deleteBookmarkFolder: (Set<BookmarkNode>) -> Unit,
|
||||
private val invokePendingDeletion: () -> Unit
|
||||
) : BookmarkController {
|
||||
|
||||
|
@ -133,8 +133,8 @@ class DefaultBookmarkController(
|
|||
deleteBookmarkNodes(nodes, eventType)
|
||||
}
|
||||
|
||||
override fun handleBookmarkFolderDeletion(node: BookmarkNode) {
|
||||
deleteBookmarkFolder(node)
|
||||
override fun handleBookmarkFolderDeletion(nodes: Set<BookmarkNode>) {
|
||||
deleteBookmarkFolder(nodes)
|
||||
}
|
||||
|
||||
override fun handleRequestSync() {
|
||||
|
|
|
@ -283,13 +283,17 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
|||
}
|
||||
|
||||
private fun deleteMulti(selected: Set<BookmarkNode>, eventType: Event = Event.RemoveBookmarks) {
|
||||
selected.forEach { if (it.type == BookmarkNodeType.FOLDER) {
|
||||
showRemoveFolderDialog(selected)
|
||||
return
|
||||
} }
|
||||
updatePendingBookmarksToDelete(selected)
|
||||
|
||||
pendingBookmarkDeletionJob = getDeleteOperation(eventType)
|
||||
|
||||
val message = when (eventType) {
|
||||
is Event.RemoveBookmarks -> {
|
||||
getRemoveBookmarksSnackBarMessage(selected)
|
||||
getRemoveBookmarksSnackBarMessage(selected, containsFolders = false)
|
||||
}
|
||||
is Event.RemoveBookmarkFolder,
|
||||
is Event.RemoveBookmark -> {
|
||||
|
@ -310,9 +314,16 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
|||
)
|
||||
}
|
||||
|
||||
private fun getRemoveBookmarksSnackBarMessage(selected: Set<BookmarkNode>): String {
|
||||
private fun getRemoveBookmarksSnackBarMessage(
|
||||
selected: Set<BookmarkNode>,
|
||||
containsFolders: Boolean
|
||||
): String {
|
||||
return if (selected.size > 1) {
|
||||
getString(R.string.bookmark_deletion_multiple_snackbar_message_2)
|
||||
return if (containsFolders) {
|
||||
getString(R.string.bookmark_deletion_multiple_snackbar_message_3)
|
||||
} else {
|
||||
getString(R.string.bookmark_deletion_multiple_snackbar_message_2)
|
||||
}
|
||||
} else {
|
||||
val bookmarkNode = selected.first()
|
||||
getString(
|
||||
|
@ -323,29 +334,38 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
|||
}
|
||||
}
|
||||
|
||||
private fun getDialogConfirmationMessage(selected: Set<BookmarkNode>): String {
|
||||
return if (selected.size > 1) {
|
||||
getString(R.string.bookmark_delete_multiple_folders_confirmation_dialog, getString(R.string.app_name))
|
||||
} else {
|
||||
getString(R.string.bookmark_delete_folder_confirmation_dialog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_bookmarkInteractor = null
|
||||
}
|
||||
|
||||
private fun showRemoveFolderDialog(selected: BookmarkNode) {
|
||||
private fun showRemoveFolderDialog(selected: Set<BookmarkNode>) {
|
||||
activity?.let { activity ->
|
||||
AlertDialog.Builder(activity).apply {
|
||||
setMessage(R.string.bookmark_delete_folder_confirmation_dialog)
|
||||
val dialogConfirmationMessage = getDialogConfirmationMessage(selected)
|
||||
setMessage(dialogConfirmationMessage)
|
||||
setNegativeButton(R.string.delete_browsing_data_prompt_cancel) { dialog: DialogInterface, _ ->
|
||||
dialog.cancel()
|
||||
}
|
||||
setPositiveButton(R.string.delete_browsing_data_prompt_allow) { dialog: DialogInterface, _ ->
|
||||
updatePendingBookmarksToDelete(setOf(selected))
|
||||
updatePendingBookmarksToDelete(selected)
|
||||
pendingBookmarkDeletionJob = getDeleteOperation(Event.RemoveBookmarkFolder)
|
||||
dialog.dismiss()
|
||||
val message = getDeleteDialogString(selected)
|
||||
val snackbarMessage = getRemoveBookmarksSnackBarMessage(selected, containsFolders = true)
|
||||
viewLifecycleOwner.lifecycleScope.allowUndo(
|
||||
requireView(),
|
||||
message,
|
||||
snackbarMessage,
|
||||
getString(R.string.bookmark_undo_deletion),
|
||||
{
|
||||
undoPendingDeletion(setOf(selected))
|
||||
undoPendingDeletion(selected)
|
||||
},
|
||||
operation = getDeleteOperation(Event.RemoveBookmarkFolder)
|
||||
)
|
||||
|
@ -362,14 +382,6 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
|||
bookmarkInteractor.onBookmarksChanged(bookmarkTree)
|
||||
}
|
||||
|
||||
private fun getDeleteDialogString(selected: BookmarkNode): String {
|
||||
return getString(
|
||||
R.string.bookmark_deletion_snackbar_message,
|
||||
context?.components?.publicSuffixList?.let { selected.url?.toShortUrl(it) }
|
||||
?: selected.title
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun undoPendingDeletion(selected: Set<BookmarkNode>) {
|
||||
pendingBookmarksToDelete.removeAll(selected)
|
||||
pendingBookmarkDeletionJob = null
|
||||
|
|
|
@ -88,7 +88,7 @@ class BookmarkFragmentInteractor(
|
|||
null -> Event.RemoveBookmarks
|
||||
}
|
||||
if (eventType == Event.RemoveBookmarkFolder) {
|
||||
bookmarksController.handleBookmarkFolderDeletion(nodes.first())
|
||||
bookmarksController.handleBookmarkFolderDeletion(nodes)
|
||||
} else {
|
||||
bookmarksController.handleBookmarkDeletion(nodes, eventType)
|
||||
}
|
||||
|
|
|
@ -33,6 +33,8 @@ class HistoryAdapter(
|
|||
|
||||
private var mode: HistoryFragmentState.Mode = HistoryFragmentState.Mode.Normal
|
||||
override val selectedItems get() = mode.selectedItems
|
||||
var pendingDeletionIds = emptySet<Long>()
|
||||
private val itemsWithHeaders: MutableMap<HistoryItemTimeGroup, Int> = mutableMapOf()
|
||||
|
||||
override fun getItemViewType(position: Int): Int = HistoryListItemViewHolder.LAYOUT_ID
|
||||
|
||||
|
@ -48,13 +50,33 @@ class HistoryAdapter(
|
|||
}
|
||||
|
||||
override fun onBindViewHolder(holder: HistoryListItemViewHolder, position: Int) {
|
||||
val previous = if (position == 0) null else getItem(position - 1)
|
||||
val current = getItem(position) ?: return
|
||||
val headerForCurrentItem = timeGroupForHistoryItem(current)
|
||||
val isPendingDeletion = pendingDeletionIds.contains(current.visitedAt)
|
||||
var timeGroup: HistoryItemTimeGroup? = null
|
||||
|
||||
val previousHeader = previous?.let(::timeGroupForHistoryItem)
|
||||
val currentHeader = timeGroupForHistoryItem(current)
|
||||
val timeGroup = if (currentHeader != previousHeader) currentHeader else null
|
||||
holder.bind(current, timeGroup, position == 0, mode)
|
||||
// Add or remove the header and position to the map depending on it's deletion status
|
||||
if (itemsWithHeaders.containsKey(headerForCurrentItem)) {
|
||||
if (isPendingDeletion && itemsWithHeaders[headerForCurrentItem] == position) {
|
||||
itemsWithHeaders.remove(headerForCurrentItem)
|
||||
} else if (isPendingDeletion && itemsWithHeaders[headerForCurrentItem] != position) {
|
||||
// do nothing
|
||||
} else {
|
||||
if (position <= itemsWithHeaders[headerForCurrentItem] as Int) {
|
||||
itemsWithHeaders[headerForCurrentItem] = position
|
||||
timeGroup = headerForCurrentItem
|
||||
}
|
||||
}
|
||||
} else if (!isPendingDeletion) {
|
||||
itemsWithHeaders[headerForCurrentItem] = position
|
||||
timeGroup = headerForCurrentItem
|
||||
}
|
||||
|
||||
holder.bind(current, timeGroup, position == 0, mode, isPendingDeletion)
|
||||
}
|
||||
|
||||
fun updatePendingDeletionIds(pendingDeletionIds: Set<Long>) {
|
||||
this.pendingDeletionIds = pendingDeletionIds
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -17,8 +17,11 @@ import android.view.ViewGroup
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavDirections
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.android.synthetic.main.fragment_history.view.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -31,7 +34,6 @@ import org.mozilla.fenix.HomeActivity
|
|||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.addons.showSnackBar
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
import org.mozilla.fenix.components.Components
|
||||
import org.mozilla.fenix.components.FenixSnackbar
|
||||
import org.mozilla.fenix.components.StoreProvider
|
||||
import org.mozilla.fenix.components.history.createSynchronousPagedHistoryProvider
|
||||
|
@ -42,6 +44,7 @@ import org.mozilla.fenix.ext.requireComponents
|
|||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.ext.toShortUrl
|
||||
import org.mozilla.fenix.library.LibraryPageFragment
|
||||
import org.mozilla.fenix.utils.allowUndo
|
||||
|
||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||
class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandler {
|
||||
|
@ -49,6 +52,8 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
|||
private lateinit var historyView: HistoryView
|
||||
private lateinit var historyInteractor: HistoryInteractor
|
||||
private lateinit var viewModel: HistoryViewModel
|
||||
private var undoScope: CoroutineScope? = null
|
||||
private var pendingHistoryDeletionJob: (suspend () -> Unit)? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
@ -59,7 +64,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
|||
historyStore = StoreProvider.get(this) {
|
||||
HistoryFragmentStore(
|
||||
HistoryFragmentState(
|
||||
items = listOf(), mode = HistoryFragmentState.Mode.Normal
|
||||
items = listOf(),
|
||||
mode = HistoryFragmentState.Mode.Normal,
|
||||
pendingDeletionIds = emptySet(),
|
||||
isDeletingItems = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -111,18 +119,18 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
|||
}
|
||||
|
||||
private fun deleteHistoryItems(items: Set<HistoryItem>) {
|
||||
val message = getMultiSelectSnackBarMessage(items)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
context?.components?.run {
|
||||
for (item in items) {
|
||||
analytics.metrics.track(Event.HistoryItemRemoved)
|
||||
core.historyStorage.deleteVisit(item.url, item.visitedAt)
|
||||
}
|
||||
}
|
||||
viewModel.invalidate()
|
||||
showSnackBar(requireView(), message)
|
||||
historyStore.dispatch(HistoryFragmentAction.ExitDeletionMode)
|
||||
}
|
||||
|
||||
updatePendingHistoryToDelete(items)
|
||||
undoScope = CoroutineScope(IO)
|
||||
undoScope?.allowUndo(
|
||||
requireView(),
|
||||
getMultiSelectSnackBarMessage(items),
|
||||
getString(R.string.bookmark_undo_deletion),
|
||||
{
|
||||
undoPendingDeletion(items)
|
||||
},
|
||||
getDeleteHistoryItemsOperation(items)
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
|
@ -146,8 +154,8 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
|||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
val menuRes = when (historyStore.state.mode) {
|
||||
HistoryFragmentState.Mode.Normal -> R.menu.library_menu
|
||||
is HistoryFragmentState.Mode.Syncing -> R.menu.library_menu
|
||||
is HistoryFragmentState.Mode.Editing -> R.menu.history_select_multi
|
||||
else -> return
|
||||
}
|
||||
|
||||
inflater.inflate(menuRes, menu)
|
||||
|
@ -166,13 +174,8 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
|||
true
|
||||
}
|
||||
R.id.delete_history_multi_select -> {
|
||||
val message = getMultiSelectSnackBarMessage(selectedItems)
|
||||
viewLifecycleOwner.lifecycleScope.launch(Main) {
|
||||
deleteSelectedHistory(historyStore.state.mode.selectedItems, requireComponents)
|
||||
viewModel.invalidate()
|
||||
historyStore.dispatch(HistoryFragmentAction.ExitDeletionMode)
|
||||
showSnackBar(requireView(), message)
|
||||
}
|
||||
deleteHistoryItems(historyStore.state.mode.selectedItems)
|
||||
historyStore.dispatch(HistoryFragmentAction.ExitEditMode)
|
||||
true
|
||||
}
|
||||
R.id.open_history_in_new_tabs_multi_select -> {
|
||||
|
@ -181,8 +184,7 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
|||
selectedItem.url
|
||||
}
|
||||
|
||||
nav(
|
||||
R.id.historyFragment,
|
||||
navigate(
|
||||
HistoryFragmentDirections.actionGlobalTabTrayDialogFragment()
|
||||
)
|
||||
true
|
||||
|
@ -197,8 +199,7 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
|||
browsingModeManager.mode = BrowsingMode.Private
|
||||
supportActionBar?.hide()
|
||||
}
|
||||
nav(
|
||||
R.id.historyFragment,
|
||||
navigate(
|
||||
HistoryFragmentDirections.actionGlobalTabTrayDialogFragment()
|
||||
)
|
||||
true
|
||||
|
@ -210,14 +211,23 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
|||
return if (historyItems.size > 1) {
|
||||
getString(R.string.history_delete_multiple_items_snackbar)
|
||||
} else {
|
||||
getString(
|
||||
R.string.history_delete_single_item_snackbar,
|
||||
historyItems.first().url.toShortUrl(requireComponents.publicSuffixList)
|
||||
String.format(
|
||||
requireContext().getString(
|
||||
R.string.history_delete_single_item_snackbar
|
||||
), historyItems.first().url.toShortUrl(requireComponents.publicSuffixList)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean = historyView.onBackPressed()
|
||||
override fun onPause() {
|
||||
invokePendingDeletion()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
invokePendingDeletion()
|
||||
return historyView.onBackPressed()
|
||||
}
|
||||
|
||||
private fun openItem(item: HistoryItem, mode: BrowsingMode? = null) {
|
||||
requireComponents.analytics.metrics.track(Event.HistoryItemOpened)
|
||||
|
@ -257,23 +267,58 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun deleteSelectedHistory(
|
||||
selected: Set<HistoryItem>,
|
||||
components: Components = requireComponents
|
||||
) {
|
||||
requireComponents.analytics.metrics.track(Event.HistoryItemRemoved)
|
||||
val storage = components.core.historyStorage
|
||||
for (item in selected) {
|
||||
storage.deleteVisit(item.url, item.visitedAt)
|
||||
}
|
||||
}
|
||||
|
||||
private fun share(data: List<ShareData>) {
|
||||
requireComponents.analytics.metrics.track(Event.HistoryItemShared)
|
||||
val directions = HistoryFragmentDirections.actionGlobalShareFragment(
|
||||
data = data.toTypedArray()
|
||||
)
|
||||
nav(R.id.historyFragment, directions)
|
||||
navigate(directions)
|
||||
}
|
||||
|
||||
private fun navigate(directions: NavDirections) {
|
||||
invokePendingDeletion()
|
||||
findNavController().nav(
|
||||
R.id.historyFragment,
|
||||
directions
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDeleteHistoryItemsOperation(items: Set<HistoryItem>): (suspend () -> Unit) {
|
||||
return {
|
||||
CoroutineScope(IO).launch {
|
||||
historyStore.dispatch(HistoryFragmentAction.EnterDeletionMode)
|
||||
context?.components?.run {
|
||||
for (item in items) {
|
||||
analytics.metrics.track(Event.HistoryItemRemoved)
|
||||
core.historyStorage.deleteVisit(item.url, item.visitedAt)
|
||||
}
|
||||
}
|
||||
historyStore.dispatch(HistoryFragmentAction.ExitDeletionMode)
|
||||
pendingHistoryDeletionJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePendingHistoryToDelete(items: Set<HistoryItem>) {
|
||||
pendingHistoryDeletionJob = getDeleteHistoryItemsOperation(items)
|
||||
val ids = items.map { item -> item.visitedAt }.toSet()
|
||||
historyStore.dispatch(HistoryFragmentAction.AddPendingDeletionSet(ids))
|
||||
}
|
||||
|
||||
private fun undoPendingDeletion(items: Set<HistoryItem>) {
|
||||
pendingHistoryDeletionJob = null
|
||||
val ids = items.map { item -> item.visitedAt }.toSet()
|
||||
historyStore.dispatch(HistoryFragmentAction.UndoPendingDeletionSet(ids))
|
||||
}
|
||||
|
||||
private fun invokePendingDeletion() {
|
||||
pendingHistoryDeletionJob?.let {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
it.invoke()
|
||||
}.invokeOnCompletion {
|
||||
pendingHistoryDeletionJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun syncHistory() {
|
||||
|
|
|
@ -30,6 +30,8 @@ sealed class HistoryFragmentAction : Action {
|
|||
object ExitEditMode : HistoryFragmentAction()
|
||||
data class AddItemForRemoval(val item: HistoryItem) : HistoryFragmentAction()
|
||||
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryFragmentAction()
|
||||
data class AddPendingDeletionSet(val itemIds: Set<Long>) : HistoryFragmentAction()
|
||||
data class UndoPendingDeletionSet(val itemIds: Set<Long>) : HistoryFragmentAction()
|
||||
object EnterDeletionMode : HistoryFragmentAction()
|
||||
object ExitDeletionMode : HistoryFragmentAction()
|
||||
object StartSync : HistoryFragmentAction()
|
||||
|
@ -41,12 +43,16 @@ sealed class HistoryFragmentAction : Action {
|
|||
* @property items List of HistoryItem to display
|
||||
* @property mode Current Mode of History
|
||||
*/
|
||||
data class HistoryFragmentState(val items: List<HistoryItem>, val mode: Mode) : State {
|
||||
data class HistoryFragmentState(
|
||||
val items: List<HistoryItem>,
|
||||
val mode: Mode,
|
||||
val pendingDeletionIds: Set<Long>,
|
||||
val isDeletingItems: Boolean
|
||||
) : State {
|
||||
sealed class Mode {
|
||||
open val selectedItems = emptySet<HistoryItem>()
|
||||
|
||||
object Normal : Mode()
|
||||
object Deleting : Mode()
|
||||
object Syncing : Mode()
|
||||
data class Editing(override val selectedItems: Set<HistoryItem>) : Mode()
|
||||
}
|
||||
|
@ -73,9 +79,17 @@ private fun historyStateReducer(
|
|||
)
|
||||
}
|
||||
is HistoryFragmentAction.ExitEditMode -> state.copy(mode = HistoryFragmentState.Mode.Normal)
|
||||
is HistoryFragmentAction.EnterDeletionMode -> state.copy(mode = HistoryFragmentState.Mode.Deleting)
|
||||
is HistoryFragmentAction.ExitDeletionMode -> state.copy(mode = HistoryFragmentState.Mode.Normal)
|
||||
is HistoryFragmentAction.EnterDeletionMode -> state.copy(isDeletingItems = true)
|
||||
is HistoryFragmentAction.ExitDeletionMode -> state.copy(isDeletingItems = false)
|
||||
is HistoryFragmentAction.StartSync -> state.copy(mode = HistoryFragmentState.Mode.Syncing)
|
||||
is HistoryFragmentAction.FinishSync -> state.copy(mode = HistoryFragmentState.Mode.Normal)
|
||||
is HistoryFragmentAction.AddPendingDeletionSet ->
|
||||
state.copy(
|
||||
pendingDeletionIds = state.pendingDeletionIds + action.itemIds
|
||||
)
|
||||
is HistoryFragmentAction.UndoPendingDeletionSet ->
|
||||
state.copy(
|
||||
pendingDeletionIds = state.pendingDeletionIds - action.itemIds
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,7 +90,6 @@ class HistoryView(
|
|||
val view: View = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_history, container, true)
|
||||
|
||||
private var items: List<HistoryItem> = listOf()
|
||||
var mode: HistoryFragmentState.Mode = HistoryFragmentState.Mode.Normal
|
||||
private set
|
||||
|
||||
|
@ -116,13 +115,16 @@ class HistoryView(
|
|||
fun update(state: HistoryFragmentState) {
|
||||
val oldMode = mode
|
||||
|
||||
view.progress_bar.isVisible = state.mode === HistoryFragmentState.Mode.Deleting
|
||||
view.progress_bar.isVisible = state.isDeletingItems
|
||||
view.swipe_refresh.isRefreshing = state.mode === HistoryFragmentState.Mode.Syncing
|
||||
view.swipe_refresh.isEnabled =
|
||||
state.mode === HistoryFragmentState.Mode.Normal || state.mode === HistoryFragmentState.Mode.Syncing
|
||||
items = state.items
|
||||
mode = state.mode
|
||||
|
||||
historyAdapter.updatePendingDeletionIds(state.pendingDeletionIds)
|
||||
|
||||
updateEmptyState(state.pendingDeletionIds.size != historyAdapter.currentList?.size)
|
||||
|
||||
historyAdapter.updateMode(state.mode)
|
||||
val first = layoutManager.findFirstVisibleItemPosition()
|
||||
val last = layoutManager.findLastVisibleItemPosition() + 1
|
||||
|
|
|
@ -11,13 +11,13 @@ import kotlinx.android.synthetic.main.library_site_item.view.*
|
|||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.hideAndDisable
|
||||
import org.mozilla.fenix.ext.showAndEnable
|
||||
import org.mozilla.fenix.utils.Do
|
||||
import org.mozilla.fenix.library.SelectionHolder
|
||||
import org.mozilla.fenix.library.history.HistoryFragmentState
|
||||
import org.mozilla.fenix.library.history.HistoryInteractor
|
||||
import org.mozilla.fenix.library.history.HistoryItem
|
||||
import org.mozilla.fenix.library.history.HistoryItemMenu
|
||||
import org.mozilla.fenix.library.history.HistoryItemTimeGroup
|
||||
import org.mozilla.fenix.utils.Do
|
||||
|
||||
class HistoryListItemViewHolder(
|
||||
view: View,
|
||||
|
@ -44,8 +44,15 @@ class HistoryListItemViewHolder(
|
|||
item: HistoryItem,
|
||||
timeGroup: HistoryItemTimeGroup?,
|
||||
showDeleteButton: Boolean,
|
||||
mode: HistoryFragmentState.Mode
|
||||
mode: HistoryFragmentState.Mode,
|
||||
isPendingDeletion: Boolean = false
|
||||
) {
|
||||
if (isPendingDeletion) {
|
||||
itemView.history_layout.visibility = View.GONE
|
||||
} else {
|
||||
itemView.history_layout.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
itemView.history_layout.titleView.text = item.title
|
||||
itemView.history_layout.urlView.text = item.url
|
||||
|
||||
|
|
|
@ -14,29 +14,22 @@ import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsDeleteButton
|
|||
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) {
|
||||
) : ListAdapter<LoginExceptionsAdapter.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
|
||||
)
|
||||
val adapterItems: List<AdapterItem> = listOf(AdapterItem.Header) +
|
||||
exceptions.map { AdapterItem.Item(it) } +
|
||||
listOf(AdapterItem.DeleteButton)
|
||||
submitList(adapterItems)
|
||||
}
|
||||
|
||||
|
@ -70,9 +63,18 @@ class LoginExceptionsAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
private object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
|
||||
sealed class AdapterItem {
|
||||
object DeleteButton : AdapterItem()
|
||||
object Header : AdapterItem()
|
||||
data class Item(val item: LoginException) : AdapterItem()
|
||||
}
|
||||
|
||||
internal object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
|
||||
override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
|
||||
areContentsTheSame(oldItem, newItem)
|
||||
when (oldItem) {
|
||||
AdapterItem.DeleteButton, AdapterItem.Header -> oldItem === newItem
|
||||
is AdapterItem.Item -> newItem is AdapterItem.Item && oldItem.item.id == newItem.item.id
|
||||
}
|
||||
|
||||
@Suppress("DiffUtilEquals")
|
||||
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
|
||||
|
|
|
@ -9,9 +9,9 @@ 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 androidx.lifecycle.observe
|
||||
import kotlinx.android.synthetic.main.fragment_exceptions.view.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
@ -57,18 +57,15 @@ class LoginExceptionsFragment : Fragment() {
|
|||
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)
|
||||
}
|
||||
private fun subscribeToLoginExceptions() {
|
||||
requireComponents.core.loginExceptionStorage.getLoginExceptions().asLiveData()
|
||||
.observe(viewLifecycleOwner) { exceptions ->
|
||||
exceptionsStore.dispatch(ExceptionsFragmentAction.Change(exceptions))
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
consumeFrom(exceptionsStore) {
|
||||
exceptionsView.update(it)
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ 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 kotlinx.android.synthetic.main.component_exceptions.*
|
||||
import mozilla.components.feature.logins.exceptions.LoginException
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
|
@ -34,29 +34,29 @@ interface ExceptionsViewInteractor {
|
|||
* View that contains and configures the Exceptions List
|
||||
*/
|
||||
class LoginExceptionsView(
|
||||
override val containerView: ViewGroup,
|
||||
container: ViewGroup,
|
||||
val interactor: LoginExceptionsInteractor
|
||||
) : LayoutContainer {
|
||||
|
||||
val view: FrameLayout = LayoutInflater.from(containerView.context)
|
||||
.inflate(R.layout.component_exceptions, containerView, true)
|
||||
override val containerView: FrameLayout = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_exceptions, container, 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 {
|
||||
exceptions_learn_more.isVisible = false
|
||||
exceptions_empty_message.text =
|
||||
containerView.context.getString(R.string.preferences_passwords_exceptions_description_empty)
|
||||
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()
|
||||
exceptions_empty_view.isVisible = state.items.isEmpty()
|
||||
exceptions_list.isVisible = state.items.isNotEmpty()
|
||||
exceptionsAdapter.updateData(state.items)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,12 +12,13 @@ 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)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LAYOUT_ID = R.layout.exceptions_description
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,28 +5,15 @@
|
|||
package org.mozilla.fenix.migration
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.DimenRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.activity_migration.*
|
||||
import kotlinx.android.synthetic.main.migration_list_item.view.*
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
||||
import mozilla.components.support.migration.AbstractMigrationProgressActivity
|
||||
import mozilla.components.support.migration.AbstractMigrationService
|
||||
import mozilla.components.support.migration.Migration
|
||||
import mozilla.components.support.migration.Migration.Bookmarks
|
||||
import mozilla.components.support.migration.Migration.History
|
||||
import mozilla.components.support.migration.Migration.Logins
|
||||
import mozilla.components.support.migration.Migration.Settings
|
||||
import mozilla.components.support.migration.MigrationResults
|
||||
import mozilla.components.support.migration.state.MigrationAction
|
||||
import mozilla.components.support.migration.state.MigrationProgress
|
||||
|
@ -97,91 +84,10 @@ class MigrationProgressActivity : AbstractMigrationProgressActivity() {
|
|||
migration_button.setBackgroundResource(R.drawable.migration_button_background)
|
||||
migration_button_progress_bar.visibility = View.INVISIBLE
|
||||
// Keep the results list up-to-date.
|
||||
statusAdapter.submitList(results.toItemList())
|
||||
statusAdapter.updateData(results)
|
||||
}
|
||||
|
||||
override fun onMigrationStateChanged(progress: MigrationProgress, results: MigrationResults) {
|
||||
statusAdapter.submitList(results.toItemList())
|
||||
}
|
||||
}
|
||||
|
||||
// These are the only items we want to show migrating in the UI.
|
||||
internal val whiteList = linkedMapOf(
|
||||
Settings to R.string.settings_title,
|
||||
History to R.string.preferences_sync_history,
|
||||
Bookmarks to R.string.preferences_sync_bookmarks,
|
||||
Logins to R.string.migration_text_passwords
|
||||
)
|
||||
|
||||
internal fun MigrationResults.toItemList() = whiteList.keys
|
||||
.map {
|
||||
if (containsKey(it)) {
|
||||
MigrationItem(it, getValue(it).success)
|
||||
} else {
|
||||
MigrationItem(it)
|
||||
}
|
||||
}
|
||||
|
||||
internal data class MigrationItem(val migration: Migration, val status: Boolean = false)
|
||||
|
||||
internal class MigrationStatusAdapter :
|
||||
ListAdapter<MigrationItem, MigrationStatusAdapter.ViewHolder>(DiffCallback) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.migration_list_item
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
private val context = view.context
|
||||
private val title = view.migration_item_name
|
||||
private val status = view.migration_status_image
|
||||
|
||||
fun bind(item: MigrationItem) {
|
||||
// Get the resource ID for the item.
|
||||
val migrationText = whiteList[item.migration]?.run {
|
||||
context.getString(this)
|
||||
} ?: ""
|
||||
title.text = migrationText
|
||||
status.visibility = if (item.status) View.VISIBLE else View.INVISIBLE
|
||||
status.contentDescription = context.getString(R.string.migration_icon_description)
|
||||
}
|
||||
}
|
||||
|
||||
private object DiffCallback : DiffUtil.ItemCallback<MigrationItem>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: MigrationItem, newItem: MigrationItem) =
|
||||
oldItem.migration.javaClass.simpleName == newItem.migration.javaClass.simpleName
|
||||
|
||||
override fun areContentsTheSame(oldItem: MigrationItem, newItem: MigrationItem) =
|
||||
oldItem.migration.javaClass.simpleName == newItem.migration.javaClass.simpleName &&
|
||||
oldItem.status == newItem.status
|
||||
}
|
||||
}
|
||||
|
||||
internal class MigrationStatusItemDecoration(
|
||||
@DimenRes private val spacing: Int
|
||||
) : RecyclerView.ItemDecoration() {
|
||||
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
val position = parent.getChildViewHolder(view).adapterPosition
|
||||
val itemCount = state.itemCount
|
||||
|
||||
outRect.left = spacing
|
||||
outRect.right = spacing
|
||||
outRect.top = spacing
|
||||
outRect.bottom = if (position == itemCount - 1) spacing else 0
|
||||
statusAdapter.updateData(results)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
/* 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.migration
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.Px
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.migration_list_item.view.*
|
||||
import mozilla.components.support.migration.Migration
|
||||
import mozilla.components.support.migration.MigrationResults
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
internal data class MigrationItem(
|
||||
val migration: Migration,
|
||||
val status: Boolean = false
|
||||
)
|
||||
|
||||
// These are the only items we want to show migrating in the UI.
|
||||
internal val whiteList = linkedMapOf(
|
||||
Migration.Settings to R.string.settings_title,
|
||||
Migration.History to R.string.preferences_sync_history,
|
||||
Migration.Bookmarks to R.string.preferences_sync_bookmarks,
|
||||
Migration.Logins to R.string.migration_text_passwords
|
||||
)
|
||||
|
||||
internal class MigrationStatusAdapter :
|
||||
ListAdapter<MigrationItem, MigrationStatusAdapter.ViewHolder>(DiffCallback) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.migration_list_item, parent, false)
|
||||
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the [results] to only include items in [whiteList] and update the adapter.
|
||||
*/
|
||||
fun updateData(results: MigrationResults) {
|
||||
val itemList = whiteList.keys.map {
|
||||
if (results.containsKey(it)) {
|
||||
MigrationItem(it, results.getValue(it).success)
|
||||
} else {
|
||||
MigrationItem(it)
|
||||
}
|
||||
}
|
||||
submitList(itemList)
|
||||
}
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
private val context = view.context
|
||||
private val title = view.migration_item_name
|
||||
private val status = view.migration_status_image
|
||||
|
||||
fun bind(item: MigrationItem) {
|
||||
// Get the resource ID for the item.
|
||||
val migrationText = whiteList[item.migration]?.let {
|
||||
context.getString(it)
|
||||
}.orEmpty()
|
||||
title.text = migrationText
|
||||
status.isInvisible = !item.status
|
||||
status.contentDescription = context.getString(R.string.migration_icon_description)
|
||||
}
|
||||
}
|
||||
|
||||
private object DiffCallback : DiffUtil.ItemCallback<MigrationItem>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: MigrationItem, newItem: MigrationItem) =
|
||||
oldItem.migration.javaClass.simpleName == newItem.migration.javaClass.simpleName
|
||||
|
||||
override fun areContentsTheSame(oldItem: MigrationItem, newItem: MigrationItem) =
|
||||
oldItem.migration.javaClass.simpleName == newItem.migration.javaClass.simpleName &&
|
||||
oldItem.status == newItem.status
|
||||
}
|
||||
}
|
||||
|
||||
internal class MigrationStatusItemDecoration(
|
||||
@Px private val spacing: Int
|
||||
) : RecyclerView.ItemDecoration() {
|
||||
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
val position = parent.getChildViewHolder(view).adapterPosition
|
||||
val itemCount = state.itemCount
|
||||
|
||||
outRect.left = spacing
|
||||
outRect.right = spacing
|
||||
outRect.top = spacing
|
||||
outRect.bottom = if (position == itemCount - 1) spacing else 0
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/* 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/. */
|
||||
* 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.migration
|
||||
|
||||
|
@ -15,7 +15,8 @@ import org.mozilla.fenix.components.metrics.MetricController
|
|||
|
||||
class MigrationTelemetryListener(
|
||||
private val metrics: MetricController,
|
||||
private val store: MigrationStore
|
||||
private val store: MigrationStore,
|
||||
private val logger: Logger = Logger("MigrationTelemetryListener")
|
||||
) {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
@ -23,7 +24,7 @@ class MigrationTelemetryListener(
|
|||
// Observe for migration completed.
|
||||
store.flowScoped { flow ->
|
||||
flow.collect { state ->
|
||||
Logger("MigrationTelemetryListener").debug("Migration state: ${state.progress}")
|
||||
logger.debug("Migration state: ${state.progress}")
|
||||
if (state.progress == MigrationProgress.COMPLETED) {
|
||||
metrics.track(Event.FennecToFenixMigrated)
|
||||
}
|
||||
|
|
|
@ -74,6 +74,6 @@ object Performance {
|
|||
* Disables the first time PWA popup.
|
||||
*/
|
||||
private fun disableFirstTimePWAPopup(context: Context) {
|
||||
Settings.getInstance(context).userKnowsAboutPWAs = true
|
||||
Settings.getInstance(context).userKnowsAboutPwas = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -316,11 +316,20 @@ class SearchFragment : Fragment(), UserInteractionHandler {
|
|||
updateSearchWithLabel(it)
|
||||
updateClipboardSuggestion(it, requireContext().components.clipboardHandler.url)
|
||||
updateSearchSuggestionsHintVisibility(it)
|
||||
updateToolbarContentDescription(it)
|
||||
}
|
||||
|
||||
startPostponedEnterTransition()
|
||||
}
|
||||
|
||||
private fun updateToolbarContentDescription(searchState: SearchFragmentState) {
|
||||
val urlView = toolbarView.view
|
||||
.findViewById<InlineAutocompleteEditText>(R.id.mozac_browser_toolbar_edit_url_view)
|
||||
toolbarView.view.contentDescription =
|
||||
searchState.searchEngineSource.searchEngine.name + ", " + urlView.hint
|
||||
urlView?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
/* 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.settings.logins
|
||||
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.fragment_login_detail.*
|
||||
|
||||
/**
|
||||
* View that contains and configures the Login Details
|
||||
*/
|
||||
class LoginDetailView(override val containerView: ViewGroup?) : LayoutContainer {
|
||||
fun update(login: LoginsListState) {
|
||||
webAddressText.text = login.currentItem?.origin
|
||||
usernameText.text = login.currentItem?.username
|
||||
passwordText.text = login.currentItem?.password
|
||||
}
|
||||
}
|
|
@ -54,18 +54,20 @@ sealed class LoginsAction : Action {
|
|||
data class UpdateLoginsList(val list: List<SavedLogin>) : LoginsAction()
|
||||
data class UpdateCurrentLogin(val item: SavedLogin) : LoginsAction()
|
||||
data class SortLogins(val sortingStrategy: SortingStrategy) : LoginsAction()
|
||||
data class ListOfDupes(val dupeList: List<SavedLogin>) : LoginsAction()
|
||||
data class LoginSelected(val item: SavedLogin) : LoginsAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* The state for the Saved Logins Screen
|
||||
* @property loginList Source of truth for local list of logins
|
||||
* @property loginList Filterable list of logins to display
|
||||
* @property currentItem The last item that was opened into the detail view
|
||||
* @property searchedForText String used by the user to filter logins
|
||||
* @property sortingStrategy sorting strategy selected by the user (Currently we support
|
||||
* sorting alphabetically and by last used)
|
||||
* @property highlightedItem The current selected sorting strategy from the sort menu
|
||||
* @property duplicateLogins The current list of possible duplicates for a selected login origin,
|
||||
* httpRealm, and formActionOrigin
|
||||
*/
|
||||
data class LoginsListState(
|
||||
val isLoading: Boolean = false,
|
||||
|
@ -74,7 +76,8 @@ data class LoginsListState(
|
|||
val currentItem: SavedLogin? = null,
|
||||
val searchedForText: String?,
|
||||
val sortingStrategy: SortingStrategy,
|
||||
val highlightedItem: SavedLoginsSortingStrategyMenu.Item
|
||||
val highlightedItem: SavedLoginsSortingStrategyMenu.Item,
|
||||
val duplicateLogins: List<SavedLogin>
|
||||
) : State
|
||||
|
||||
/**
|
||||
|
@ -113,9 +116,14 @@ private fun savedLoginsStateReducer(
|
|||
}
|
||||
is LoginsAction.LoginSelected -> {
|
||||
state.copy(
|
||||
isLoading = true,
|
||||
loginList = emptyList(),
|
||||
filteredItems = emptyList()
|
||||
isLoading = true,
|
||||
loginList = emptyList(),
|
||||
filteredItems = emptyList()
|
||||
)
|
||||
}
|
||||
is LoginsAction.ListOfDupes -> {
|
||||
state.copy(
|
||||
duplicateLogins = action.dupeList
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
/* 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.settings.logins
|
||||
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.navigation.NavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.component_saved_logins.view.*
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.ext.addUnderline
|
||||
import org.mozilla.fenix.settings.SupportUtils
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
|
||||
/**
|
||||
* View that contains and configures the Saved Logins List
|
||||
*/
|
||||
class SavedLoginsView(
|
||||
override val containerView: ViewGroup,
|
||||
val interactor: SavedLoginsInteractor
|
||||
) : LayoutContainer {
|
||||
|
||||
val view: FrameLayout = LayoutInflater.from(containerView.context)
|
||||
.inflate(R.layout.component_saved_logins, containerView, true)
|
||||
.findViewById(R.id.saved_logins_wrapper)
|
||||
|
||||
private val loginsAdapter = LoginsAdapter(interactor)
|
||||
|
||||
init {
|
||||
view.saved_logins_list.apply {
|
||||
adapter = loginsAdapter
|
||||
layoutManager = LinearLayoutManager(containerView.context)
|
||||
itemAnimator = null
|
||||
}
|
||||
|
||||
with(view.saved_passwords_empty_learn_more) {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
addUnderline()
|
||||
setOnClickListener { interactor.onLearnMoreClicked() }
|
||||
}
|
||||
|
||||
with(view.saved_passwords_empty_message) {
|
||||
val appName = context.getString(R.string.app_name)
|
||||
text = String.format(
|
||||
context.getString(
|
||||
R.string.preferences_passwords_saved_logins_description_empty_text
|
||||
), appName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun update(state: LoginsListState) {
|
||||
// todo MVI views should not have logic. Needs refactoring.
|
||||
if (state.isLoading) {
|
||||
view.progress_bar.isVisible = true
|
||||
} else {
|
||||
view.progress_bar.isVisible = false
|
||||
view.saved_logins_list.isVisible = state.loginList.isNotEmpty()
|
||||
view.saved_passwords_empty_view.isVisible = state.loginList.isEmpty()
|
||||
}
|
||||
loginsAdapter.submitList(state.filteredItems)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactor for the saved logins screen
|
||||
*
|
||||
* @param savedLoginsController [SavedLoginsController] which will be delegated for all users interactions.
|
||||
*/
|
||||
class SavedLoginsInteractor(
|
||||
private val savedLoginsController: SavedLoginsController
|
||||
) {
|
||||
fun onItemClicked(item: SavedLogin) {
|
||||
savedLoginsController.handleItemClicked(item)
|
||||
}
|
||||
|
||||
fun onLearnMoreClicked() {
|
||||
savedLoginsController.handleLearnMoreClicked()
|
||||
}
|
||||
|
||||
fun onSortingStrategyChanged(sortingStrategy: SortingStrategy) {
|
||||
savedLoginsController.handleSort(sortingStrategy)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller for the saved logins screen
|
||||
*
|
||||
* @param store Store used to hold in-memory collection state.
|
||||
* @param navController NavController manages app navigation within a NavHost.
|
||||
* @param browserNavigator Controller allowing browser navigation to any Uri.
|
||||
* @param settings SharedPreferences wrapper for easier usage.
|
||||
* @param metrics Controller that handles telemetry events.
|
||||
*/
|
||||
class SavedLoginsController(
|
||||
private val store: LoginsFragmentStore,
|
||||
private val navController: NavController,
|
||||
private val browserNavigator: (
|
||||
searchTermOrURL: String,
|
||||
newTab: Boolean,
|
||||
from: BrowserDirection
|
||||
) -> Unit,
|
||||
private val settings: Settings,
|
||||
private val metrics: MetricController
|
||||
) {
|
||||
fun handleSort(sortingStrategy: SortingStrategy) {
|
||||
store.dispatch(LoginsAction.SortLogins(sortingStrategy))
|
||||
settings.savedLoginsSortingStrategy = sortingStrategy
|
||||
}
|
||||
|
||||
fun handleItemClicked(item: SavedLogin) {
|
||||
store.dispatch(LoginsAction.LoginSelected(item))
|
||||
metrics.track(Event.OpenOneLogin)
|
||||
navController.navigate(
|
||||
SavedLoginsFragmentDirections.actionSavedLoginsFragmentToLoginDetailFragment(item.guid)
|
||||
)
|
||||
}
|
||||
|
||||
fun handleLearnMoreClicked() {
|
||||
browserNavigator.invoke(
|
||||
SupportUtils.getGenericSumoURLForTopic(SupportUtils.SumoTopic.SYNC_SETUP),
|
||||
true,
|
||||
BrowserDirection.FromSavedLoginsFragment
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/* 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.settings.logins.controller
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.settings.SupportUtils
|
||||
import org.mozilla.fenix.settings.logins.LoginsAction
|
||||
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
|
||||
import org.mozilla.fenix.settings.logins.SavedLogin
|
||||
import org.mozilla.fenix.settings.logins.SortingStrategy
|
||||
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsFragmentDirections
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
|
||||
/**
|
||||
* Controller for the saved logins list
|
||||
*
|
||||
* @param loginsFragmentStore Store used to hold in-memory collection state.
|
||||
* @param navController NavController manages app navigation within a NavHost.
|
||||
* @param browserNavigator Controller allowing browser navigation to any Uri.
|
||||
* @param settings SharedPreferences wrapper for easier usage.
|
||||
* @param metrics Controller that handles telemetry events.
|
||||
*/
|
||||
class LoginsListController(
|
||||
private val loginsFragmentStore: LoginsFragmentStore,
|
||||
private val navController: NavController,
|
||||
private val browserNavigator: (
|
||||
searchTermOrURL: String,
|
||||
newTab: Boolean,
|
||||
from: BrowserDirection
|
||||
) -> Unit,
|
||||
private val settings: Settings,
|
||||
private val metrics: MetricController
|
||||
) {
|
||||
|
||||
fun handleItemClicked(item: SavedLogin) {
|
||||
loginsFragmentStore.dispatch(LoginsAction.LoginSelected(item))
|
||||
metrics.track(Event.OpenOneLogin)
|
||||
navController.navigate(
|
||||
SavedLoginsFragmentDirections.actionSavedLoginsFragmentToLoginDetailFragment(item.guid)
|
||||
)
|
||||
}
|
||||
|
||||
fun handleLearnMoreClicked() {
|
||||
browserNavigator.invoke(
|
||||
SupportUtils.getGenericSumoURLForTopic(SupportUtils.SumoTopic.SYNC_SETUP),
|
||||
true,
|
||||
BrowserDirection.FromSavedLoginsFragment
|
||||
)
|
||||
}
|
||||
|
||||
fun handleSort(sortingStrategy: SortingStrategy) {
|
||||
loginsFragmentStore.dispatch(
|
||||
LoginsAction.SortLogins(
|
||||
sortingStrategy
|
||||
)
|
||||
)
|
||||
settings.savedLoginsSortingStrategy = sortingStrategy
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
/* 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.settings.logins.controller
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.navigation.NavController
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.concept.storage.Login
|
||||
import mozilla.components.service.sync.logins.InvalidRecordException
|
||||
import mozilla.components.service.sync.logins.LoginsStorageException
|
||||
import mozilla.components.service.sync.logins.NoSuchRecordException
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.settings.logins.LoginsAction
|
||||
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
|
||||
import org.mozilla.fenix.settings.logins.fragment.EditLoginFragmentDirections
|
||||
import org.mozilla.fenix.settings.logins.mapToSavedLogin
|
||||
|
||||
/**
|
||||
* Controller for all saved logins interactions with the password storage component
|
||||
*/
|
||||
open class SavedLoginsStorageController(
|
||||
private val context: Context,
|
||||
private val viewLifecycleScope: CoroutineScope,
|
||||
private val navController: NavController,
|
||||
private val loginsFragmentStore: LoginsFragmentStore
|
||||
) {
|
||||
|
||||
private suspend fun getLogin(loginId: String): Login? =
|
||||
context.components.core.passwordsStorage.get(loginId)
|
||||
|
||||
fun delete(loginId: String) {
|
||||
var deleteLoginJob: Deferred<Boolean>? = null
|
||||
val deleteJob = viewLifecycleScope.launch(Dispatchers.IO) {
|
||||
deleteLoginJob = async {
|
||||
context.components.core.passwordsStorage.delete(loginId)
|
||||
}
|
||||
deleteLoginJob?.await()
|
||||
withContext(Dispatchers.Main) {
|
||||
navController.popBackStack(R.id.savedLoginsFragment, false)
|
||||
}
|
||||
}
|
||||
deleteJob.invokeOnCompletion {
|
||||
if (it is CancellationException) {
|
||||
deleteLoginJob?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun save(loginId: String, usernameText: String, passwordText: String) {
|
||||
var saveLoginJob: Deferred<Unit>? = null
|
||||
viewLifecycleScope.launch(Dispatchers.IO) {
|
||||
saveLoginJob = async {
|
||||
// must retrieve from storage to get the httpsRealm and formActionOrigin
|
||||
val oldLogin = context.components.core.passwordsStorage.get(loginId)
|
||||
|
||||
// Update requires a Login type, which needs at least one of
|
||||
// httpRealm or formActionOrigin
|
||||
val loginToSave = Login(
|
||||
guid = loginId,
|
||||
origin = oldLogin?.origin!!,
|
||||
username = usernameText, // new value
|
||||
password = passwordText, // new value
|
||||
httpRealm = oldLogin.httpRealm,
|
||||
formActionOrigin = oldLogin.formActionOrigin
|
||||
)
|
||||
|
||||
save(loginToSave)
|
||||
syncAndUpdateList(loginToSave)
|
||||
}
|
||||
saveLoginJob?.await()
|
||||
withContext(Dispatchers.Main) {
|
||||
val directions =
|
||||
EditLoginFragmentDirections.actionEditLoginFragmentToLoginDetailFragment(
|
||||
loginId
|
||||
)
|
||||
navController.navigate(directions)
|
||||
}
|
||||
}
|
||||
saveLoginJob?.invokeOnCompletion {
|
||||
if (it is CancellationException) {
|
||||
saveLoginJob?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun save(loginToSave: Login) {
|
||||
try {
|
||||
context.components.core.passwordsStorage.update(loginToSave)
|
||||
} catch (loginException: LoginsStorageException) {
|
||||
when (loginException) {
|
||||
is NoSuchRecordException,
|
||||
is InvalidRecordException -> {
|
||||
Log.e("Edit login",
|
||||
"Failed to save edited login.", loginException)
|
||||
}
|
||||
else -> Log.e("Edit login",
|
||||
"Failed to save edited login.", loginException)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncAndUpdateList(updatedLogin: Login) {
|
||||
val login = updatedLogin.mapToSavedLogin()
|
||||
loginsFragmentStore.dispatch(
|
||||
LoginsAction.UpdateLoginsList(
|
||||
listOf(login)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun findPotentialDuplicates(loginId: String) {
|
||||
var deferredLogin: Deferred<List<Login>>? = null
|
||||
// What scope should be used here?
|
||||
val fetchLoginJob = viewLifecycleScope.launch(Dispatchers.IO) {
|
||||
deferredLogin = async {
|
||||
val login = getLogin(loginId)
|
||||
context.components.core.passwordsStorage.getPotentialDupesIgnoringUsername(login!!)
|
||||
}
|
||||
val fetchedDuplicatesList = deferredLogin?.await()
|
||||
fetchedDuplicatesList?.let { list ->
|
||||
withContext(Dispatchers.Main) {
|
||||
val savedLoginList = list.map { it.mapToSavedLogin() }
|
||||
loginsFragmentStore.dispatch(
|
||||
LoginsAction.ListOfDupes(
|
||||
savedLoginList
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchLoginJob.invokeOnCompletion {
|
||||
if (it is CancellationException) {
|
||||
deferredLogin?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchLoginDetails(loginId: String) {
|
||||
var deferredLogin: Deferred<List<Login>>? = null
|
||||
val fetchLoginJob = viewLifecycleScope.launch(Dispatchers.IO) {
|
||||
deferredLogin = async {
|
||||
context.components.core.passwordsStorage.list()
|
||||
}
|
||||
val fetchedLoginList = deferredLogin?.await()
|
||||
|
||||
fetchedLoginList?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
val login = fetchedLoginList.filter {
|
||||
it.guid == loginId
|
||||
}.first()
|
||||
loginsFragmentStore.dispatch(
|
||||
LoginsAction.UpdateCurrentLogin(
|
||||
login.mapToSavedLogin()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchLoginJob.invokeOnCompletion {
|
||||
if (it is CancellationException) {
|
||||
deferredLogin?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleLoadAndMapLogins() {
|
||||
var deferredLogins: Deferred<List<Login>>? = null
|
||||
val fetchLoginsJob = viewLifecycleScope.launch(Dispatchers.IO) {
|
||||
deferredLogins = async {
|
||||
context.components.core.passwordsStorage.list()
|
||||
}
|
||||
val logins = deferredLogins?.await()
|
||||
logins?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
loginsFragmentStore.dispatch(
|
||||
LoginsAction.UpdateLoginsList(
|
||||
logins.map { it.mapToSavedLogin() })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchLoginsJob.invokeOnCompletion {
|
||||
if (it is CancellationException) {
|
||||
deferredLogins?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,59 +2,72 @@
|
|||
* 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.settings.logins
|
||||
package org.mozilla.fenix.settings.logins.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.InputType
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.appcompat.view.menu.ActionMenuItemView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import kotlinx.android.synthetic.main.fragment_edit_login.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.concept.storage.Login
|
||||
import mozilla.components.service.sync.logins.InvalidRecordException
|
||||
import mozilla.components.service.sync.logins.LoginsStorageException
|
||||
import mozilla.components.service.sync.logins.NoSuchRecordException
|
||||
import kotlinx.android.synthetic.main.fragment_edit_login.view.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import mozilla.components.support.ktx.android.view.hideKeyboard
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.StoreProvider
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.redirectToReAuth
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.settings.logins.LoginsAction
|
||||
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
|
||||
import org.mozilla.fenix.settings.logins.LoginsListState
|
||||
import org.mozilla.fenix.settings.logins.SavedLogin
|
||||
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
||||
import org.mozilla.fenix.settings.logins.interactor.EditLoginInteractor
|
||||
import org.mozilla.fenix.settings.logins.view.EditLoginView
|
||||
|
||||
/**
|
||||
* Displays the editable saved login information for a single website.
|
||||
* Displays the editable saved login information for a single website
|
||||
*/
|
||||
@ExperimentalCoroutinesApi
|
||||
@Suppress("TooManyFunctions", "NestedBlockDepth", "ForbiddenComment")
|
||||
class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
|
||||
|
||||
private val args by navArgs<EditLoginFragmentArgs>()
|
||||
private lateinit var savedLoginsStore: LoginsFragmentStore
|
||||
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
|
||||
|
||||
private val args by navArgs<EditLoginFragmentArgs>()
|
||||
private lateinit var loginsFragmentStore: LoginsFragmentStore
|
||||
private lateinit var interactor: EditLoginInteractor
|
||||
private lateinit var editLoginView: EditLoginView
|
||||
private lateinit var oldLogin: SavedLogin
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
private var listOfPossibleDupes: List<SavedLogin>? = null
|
||||
|
||||
private var usernameChanged = false
|
||||
private var passwordChanged = false
|
||||
private var saveEnabled = false
|
||||
private var showPassword = true
|
||||
|
||||
private var validPassword = true
|
||||
private var validUsername = true
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
oldLogin = args.savedLoginItem
|
||||
savedLoginsStore = StoreProvider.get(this) {
|
||||
editLoginView = EditLoginView(view.editLoginLayout)
|
||||
|
||||
loginsFragmentStore = StoreProvider.get(this) {
|
||||
LoginsFragmentStore(
|
||||
LoginsListState(
|
||||
isLoading = true,
|
||||
|
@ -62,31 +75,59 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
|
|||
filteredItems = listOf(),
|
||||
searchedForText = null,
|
||||
sortingStrategy = requireContext().settings().savedLoginsSortingStrategy,
|
||||
highlightedItem = requireContext().settings().savedLoginsMenuHighlightedItem
|
||||
highlightedItem = requireContext().settings().savedLoginsMenuHighlightedItem,
|
||||
duplicateLogins = listOf()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
interactor = EditLoginInteractor(
|
||||
SavedLoginsStorageController(
|
||||
context = requireContext(),
|
||||
viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
|
||||
navController = findNavController(),
|
||||
loginsFragmentStore = loginsFragmentStore
|
||||
)
|
||||
)
|
||||
|
||||
// ensure hostname isn't editable
|
||||
loginsFragmentStore.dispatch(LoginsAction.UpdateCurrentLogin(args.savedLoginItem))
|
||||
interactor.findPotentialDuplicates(args.savedLoginItem.guid)
|
||||
|
||||
// initialize editable values
|
||||
hostnameText.text = args.savedLoginItem.origin.toEditable()
|
||||
hostnameText.isClickable = false
|
||||
hostnameText.isFocusable = false
|
||||
|
||||
usernameText.text = args.savedLoginItem.username.toEditable()
|
||||
passwordText.text = args.savedLoginItem.password.toEditable()
|
||||
|
||||
formatEditableValues()
|
||||
initSaveState()
|
||||
setUpClickListeners()
|
||||
setUpTextListeners()
|
||||
editLoginView.showPassword()
|
||||
|
||||
consumeFrom(loginsFragmentStore) {
|
||||
listOfPossibleDupes = loginsFragmentStore.state.duplicateLogins
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSaveState() {
|
||||
saveEnabled = false // don't enable saving until something has been changed
|
||||
val saveButton =
|
||||
activity?.findViewById<ActionMenuItemView>(R.id.save_login_button)
|
||||
saveButton?.isEnabled = saveEnabled
|
||||
|
||||
usernameChanged = false
|
||||
passwordChanged = false
|
||||
}
|
||||
|
||||
private fun formatEditableValues() {
|
||||
hostnameText.isClickable = false
|
||||
hostnameText.isFocusable = false
|
||||
usernameText.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
||||
// TODO: extend PasswordTransformationMethod() to change bullets to asterisks
|
||||
passwordText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
passwordText.compoundDrawablePadding =
|
||||
requireContext().resources
|
||||
.getDimensionPixelOffset(R.dimen.saved_logins_end_icon_drawable_padding)
|
||||
|
||||
setUpClickListeners()
|
||||
setUpTextListeners()
|
||||
}
|
||||
|
||||
private fun setUpClickListeners() {
|
||||
|
@ -105,14 +146,11 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
|
|||
it.isEnabled = false
|
||||
}
|
||||
revealPasswordButton.setOnClickListener {
|
||||
togglePasswordReveal()
|
||||
}
|
||||
|
||||
var firstClick = true
|
||||
passwordText.setOnClickListener {
|
||||
if (firstClick) {
|
||||
togglePasswordReveal()
|
||||
firstClick = false
|
||||
showPassword = !showPassword
|
||||
if (showPassword) {
|
||||
editLoginView.showPassword()
|
||||
} else {
|
||||
editLoginView.hidePassword()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -124,7 +162,6 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
|
|||
view?.hideKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
editLoginLayout.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
view?.hideKeyboard()
|
||||
|
@ -133,13 +170,20 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
|
|||
|
||||
usernameText.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(u: Editable?) {
|
||||
if (u.toString() == oldLogin.username) {
|
||||
inputLayoutUsername.error = null
|
||||
inputLayoutUsername.errorIconDrawable = null
|
||||
} else {
|
||||
clearUsernameTextButton.isEnabled = true
|
||||
// setDupeError() TODO in #10173
|
||||
when {
|
||||
u.toString() == oldLogin.username -> {
|
||||
usernameChanged = false
|
||||
validUsername = true
|
||||
inputLayoutUsername.error = null
|
||||
inputLayoutUsername.errorIconDrawable = null
|
||||
}
|
||||
else -> {
|
||||
usernameChanged = true
|
||||
clearUsernameTextButton.isEnabled = true
|
||||
setDupeError()
|
||||
}
|
||||
}
|
||||
setSaveButtonState()
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(u: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
|
@ -155,20 +199,26 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
|
|||
override fun afterTextChanged(p: Editable?) {
|
||||
when {
|
||||
p.toString().isEmpty() -> {
|
||||
passwordChanged = true
|
||||
clearPasswordTextButton.isEnabled = false
|
||||
setPasswordError()
|
||||
}
|
||||
p.toString() == oldLogin.password -> {
|
||||
passwordChanged = false
|
||||
validPassword = true
|
||||
inputLayoutPassword.error = null
|
||||
inputLayoutPassword.errorIconDrawable = null
|
||||
clearPasswordTextButton.isEnabled = true
|
||||
}
|
||||
else -> {
|
||||
passwordChanged = true
|
||||
validPassword = true
|
||||
inputLayoutPassword.error = null
|
||||
inputLayoutPassword.errorIconDrawable = null
|
||||
clearPasswordTextButton.isEnabled = true
|
||||
}
|
||||
}
|
||||
setSaveButtonState()
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(p: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
|
@ -181,14 +231,40 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
|
|||
})
|
||||
}
|
||||
|
||||
private fun isDupe(username: String): Boolean =
|
||||
loginsFragmentStore.state.duplicateLogins.filter { it.username == username }.any()
|
||||
|
||||
private fun setDupeError() {
|
||||
if (isDupe(usernameText.text.toString())) {
|
||||
inputLayoutUsername?.let {
|
||||
usernameChanged = true
|
||||
validUsername = false
|
||||
it.setErrorIconDrawable(R.drawable.mozac_ic_warning)
|
||||
it.error = context?.getString(R.string.saved_login_duplicate)
|
||||
}
|
||||
} else {
|
||||
usernameChanged = true
|
||||
validUsername = true
|
||||
inputLayoutUsername.error = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPasswordError() {
|
||||
inputLayoutPassword?.let { layout ->
|
||||
validPassword = false
|
||||
layout.error = context?.getString(R.string.saved_login_password_required)
|
||||
layout.setErrorIconDrawable(R.drawable.mozac_ic_warning)
|
||||
}
|
||||
}
|
||||
|
||||
layout.errorIconDrawable?.setTint(
|
||||
ContextCompat.getColor(requireContext(), R.color.design_default_color_error)
|
||||
)
|
||||
private fun setSaveButtonState() {
|
||||
val saveButton = activity?.findViewById<ActionMenuItemView>(R.id.save_login_button)
|
||||
val changesMadeWithNoErrors =
|
||||
validUsername && validPassword && (usernameChanged || passwordChanged)
|
||||
|
||||
changesMadeWithNoErrors.let {
|
||||
saveButton?.isEnabled = it
|
||||
saveEnabled = it
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,101 +283,16 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
|
|||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
R.id.save_login_button -> {
|
||||
view?.hideKeyboard()
|
||||
if (!passwordText.text.isNullOrBlank()) {
|
||||
try {
|
||||
attemptSaveAndExit()
|
||||
} catch (loginException: LoginsStorageException) {
|
||||
when (loginException) {
|
||||
is NoSuchRecordException,
|
||||
is InvalidRecordException -> {
|
||||
Log.e(
|
||||
"Edit login",
|
||||
"Failed to save edited login.",
|
||||
loginException
|
||||
)
|
||||
}
|
||||
else -> Log.e(
|
||||
"Edit login",
|
||||
"Failed to save edited login.",
|
||||
loginException
|
||||
)
|
||||
}
|
||||
}
|
||||
if (saveEnabled) {
|
||||
interactor.onSaveLogin(
|
||||
args.savedLoginItem.guid,
|
||||
usernameText.text.toString(),
|
||||
passwordText.text.toString()
|
||||
)
|
||||
requireComponents.analytics.metrics.track(Event.EditLoginSave)
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
// TODO: Move interactions with the component's password storage into a separate datastore
|
||||
// This includes Delete, Update/Edit, Create
|
||||
private fun attemptSaveAndExit() {
|
||||
var saveLoginJob: Deferred<Unit>? = null
|
||||
viewLifecycleOwner.lifecycleScope.launch(IO) {
|
||||
saveLoginJob = async {
|
||||
val oldLogin =
|
||||
requireContext().components.core.passwordsStorage.get(args.savedLoginItem.guid)
|
||||
|
||||
// Update requires a Login type, which needs at least one of
|
||||
// httpRealm or formActionOrigin
|
||||
val loginToSave = Login(
|
||||
guid = oldLogin?.guid,
|
||||
origin = oldLogin?.origin!!,
|
||||
username = usernameText.text.toString(), // new value
|
||||
password = passwordText.text.toString(), // new value
|
||||
httpRealm = oldLogin.httpRealm,
|
||||
formActionOrigin = oldLogin.formActionOrigin
|
||||
)
|
||||
|
||||
save(loginToSave)
|
||||
syncAndUpdateList(loginToSave)
|
||||
}
|
||||
saveLoginJob?.await()
|
||||
withContext(Main) {
|
||||
val directions =
|
||||
EditLoginFragmentDirections
|
||||
.actionEditLoginFragmentToLoginDetailFragment(args.savedLoginItem.guid)
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
}
|
||||
saveLoginJob?.invokeOnCompletion {
|
||||
if (it is CancellationException) {
|
||||
saveLoginJob?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun save(loginToSave: Login) =
|
||||
requireContext().components.core.passwordsStorage.update(loginToSave)
|
||||
|
||||
private fun syncAndUpdateList(updatedLogin: Login) {
|
||||
val login = updatedLogin.mapToSavedLogin()
|
||||
savedLoginsStore.dispatch(LoginsAction.UpdateLoginsList(listOf(login)))
|
||||
}
|
||||
|
||||
// TODO: create helper class for toggling passwords. Used in login info and edit fragments.
|
||||
private fun togglePasswordReveal() {
|
||||
val currText = passwordText.text
|
||||
if (passwordText.inputType == InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
or InputType.TYPE_CLASS_TEXT
|
||||
) {
|
||||
context?.components?.analytics?.metrics?.track(Event.ViewLoginPassword)
|
||||
passwordText.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||
revealPasswordButton.setImageDrawable(
|
||||
resources.getDrawable(R.drawable.mozac_ic_password_hide, null)
|
||||
)
|
||||
revealPasswordButton.contentDescription =
|
||||
resources.getString(R.string.saved_login_hide_password)
|
||||
} else {
|
||||
passwordText.inputType =
|
||||
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
revealPasswordButton.setImageDrawable(
|
||||
resources.getDrawable(R.drawable.mozac_ic_password_reveal, null)
|
||||
)
|
||||
revealPasswordButton.contentDescription =
|
||||
context?.getString(R.string.saved_login_reveal_password)
|
||||
}
|
||||
// For the new type to take effect you need to reset the text to it's current edited version
|
||||
passwordText?.text = currText
|
||||
}
|
||||
}
|
|
@ -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.settings.logins
|
||||
package org.mozilla.fenix.settings.logins.fragment
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
|
@ -21,16 +21,8 @@ import androidx.navigation.fragment.findNavController
|
|||
import androidx.navigation.fragment.navArgs
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.fragment_login_detail.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.ObsoleteCoroutinesApi
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.concept.storage.Login
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.FeatureFlags
|
||||
|
@ -42,9 +34,16 @@ import org.mozilla.fenix.components.metrics.Event
|
|||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.increaseTapArea
|
||||
import org.mozilla.fenix.ext.redirectToReAuth
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.ext.urlToTrimmedHost
|
||||
import org.mozilla.fenix.ext.simplifiedUrl
|
||||
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
|
||||
import org.mozilla.fenix.settings.logins.LoginsListState
|
||||
import org.mozilla.fenix.settings.logins.SavedLogin
|
||||
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
||||
import org.mozilla.fenix.settings.logins.interactor.LoginDetailInteractor
|
||||
import org.mozilla.fenix.settings.logins.view.LoginDetailView
|
||||
|
||||
/**
|
||||
* Displays saved login information for a single website.
|
||||
|
@ -57,8 +56,10 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) {
|
|||
private var login: SavedLogin? = null
|
||||
private lateinit var savedLoginsStore: LoginsFragmentStore
|
||||
private lateinit var loginDetailView: LoginDetailView
|
||||
private lateinit var interactor: LoginDetailInteractor
|
||||
private lateinit var menu: Menu
|
||||
private var deleteDialog: AlertDialog? = null
|
||||
private var showPassword = true
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
@ -74,12 +75,14 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) {
|
|||
filteredItems = listOf(),
|
||||
searchedForText = null,
|
||||
sortingStrategy = requireContext().settings().savedLoginsSortingStrategy,
|
||||
highlightedItem = requireContext().settings().savedLoginsMenuHighlightedItem
|
||||
highlightedItem = requireContext().settings().savedLoginsMenuHighlightedItem,
|
||||
duplicateLogins = listOf() // assume on load there are no dupes
|
||||
)
|
||||
)
|
||||
}
|
||||
loginDetailView = LoginDetailView(view?.findViewById(R.id.loginDetailLayout))
|
||||
fetchLoginDetails()
|
||||
loginDetailView = LoginDetailView(
|
||||
view.findViewById(R.id.loginDetailLayout)
|
||||
)
|
||||
|
||||
return view
|
||||
}
|
||||
|
@ -87,16 +90,29 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) {
|
|||
@ObsoleteCoroutinesApi
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
interactor = LoginDetailInteractor(
|
||||
SavedLoginsStorageController(
|
||||
context = requireContext(),
|
||||
viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
|
||||
navController = findNavController(),
|
||||
loginsFragmentStore = savedLoginsStore
|
||||
)
|
||||
)
|
||||
interactor.onFetchLoginList(args.savedLoginId)
|
||||
|
||||
consumeFrom(savedLoginsStore) {
|
||||
loginDetailView.update(it)
|
||||
login = savedLoginsStore.state.currentItem
|
||||
setUpCopyButtons()
|
||||
showToolbar(
|
||||
savedLoginsStore.state.currentItem?.origin?.urlToTrimmedHost(requireContext())
|
||||
savedLoginsStore.state.currentItem?.origin?.simplifiedUrl()
|
||||
?: ""
|
||||
)
|
||||
setUpPasswordReveal()
|
||||
}
|
||||
loginDetailView.togglePasswordReveal(showPassword)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -124,7 +140,11 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) {
|
|||
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
revealPasswordButton.increaseTapArea(BUTTON_INCREASE_DPS)
|
||||
revealPasswordButton.setOnClickListener {
|
||||
togglePasswordReveal()
|
||||
showPassword = !showPassword
|
||||
loginDetailView.togglePasswordReveal(!showPassword)
|
||||
}
|
||||
passwordText.setOnClickListener {
|
||||
loginDetailView.togglePasswordReveal(!showPassword)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,33 +169,6 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) {
|
|||
)
|
||||
}
|
||||
|
||||
// TODO: Move interactions with the component's password storage into a separate datastore
|
||||
private fun fetchLoginDetails() {
|
||||
var deferredLogin: Deferred<List<Login>>? = null
|
||||
val fetchLoginJob = viewLifecycleOwner.lifecycleScope.launch(IO) {
|
||||
deferredLogin = async {
|
||||
requireContext().components.core.passwordsStorage.list()
|
||||
}
|
||||
val fetchedLoginList = deferredLogin?.await()
|
||||
|
||||
fetchedLoginList?.let {
|
||||
withContext(Main) {
|
||||
val login = fetchedLoginList.filter {
|
||||
it.guid == args.savedLoginId
|
||||
}.first()
|
||||
savedLoginsStore.dispatch(
|
||||
LoginsAction.UpdateCurrentLogin(login.mapToSavedLogin())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchLoginJob.invokeOnCompletion {
|
||||
if (it is CancellationException) {
|
||||
deferredLogin?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
if (FeatureFlags.loginsEdit) {
|
||||
inflater.inflate(R.menu.login_options_menu, menu)
|
||||
|
@ -206,9 +199,11 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) {
|
|||
}
|
||||
|
||||
private fun editLogin() {
|
||||
requireComponents.analytics.metrics.track(Event.EditLogin)
|
||||
val directions =
|
||||
LoginDetailFragmentDirections
|
||||
.actionLoginDetailFragmentToEditLoginFragment(login!!)
|
||||
LoginDetailFragmentDirections.actionLoginDetailFragmentToEditLoginFragment(
|
||||
login!!
|
||||
)
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
|
||||
|
@ -220,7 +215,8 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) {
|
|||
dialog.cancel()
|
||||
}
|
||||
setPositiveButton(R.string.dialog_delete_positive) { dialog: DialogInterface, _ ->
|
||||
deleteLogin()
|
||||
requireComponents.analytics.metrics.track(Event.DeleteLogin)
|
||||
interactor.onDeleteLogin(args.savedLoginId)
|
||||
dialog.dismiss()
|
||||
}
|
||||
create()
|
||||
|
@ -228,49 +224,6 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Move interactions with the component's password storage into a separate datastore
|
||||
// This includes Delete, Update/Edit, Create
|
||||
private fun deleteLogin() {
|
||||
var deleteLoginJob: Deferred<Boolean>? = null
|
||||
val deleteJob = viewLifecycleOwner.lifecycleScope.launch(IO) {
|
||||
deleteLoginJob = async {
|
||||
requireContext().components.core.passwordsStorage.delete(args.savedLoginId)
|
||||
}
|
||||
deleteLoginJob?.await()
|
||||
withContext(Main) {
|
||||
findNavController().popBackStack(R.id.savedLoginsFragment, false)
|
||||
}
|
||||
}
|
||||
deleteJob.invokeOnCompletion {
|
||||
if (it is CancellationException) {
|
||||
deleteLoginJob?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: create helper class for toggling passwords. Used in login info and edit fragments.
|
||||
private fun togglePasswordReveal() {
|
||||
if (passwordText.inputType == InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT) {
|
||||
context?.components?.analytics?.metrics?.track(Event.ViewLoginPassword)
|
||||
passwordText.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||
revealPasswordButton.setImageDrawable(
|
||||
resources.getDrawable(R.drawable.mozac_ic_password_hide, null)
|
||||
)
|
||||
revealPasswordButton.contentDescription =
|
||||
resources.getString(R.string.saved_login_hide_password)
|
||||
} else {
|
||||
passwordText.inputType =
|
||||
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
revealPasswordButton.setImageDrawable(
|
||||
resources.getDrawable(R.drawable.mozac_ic_password_reveal, null)
|
||||
)
|
||||
revealPasswordButton.contentDescription =
|
||||
context?.getString(R.string.saved_login_reveal_password)
|
||||
}
|
||||
// For the new type to take effect you need to reset the text
|
||||
passwordText.text = login?.password
|
||||
}
|
||||
|
||||
/**
|
||||
* Click listener for a textview's copy button.
|
||||
* @param value Value to be copied
|
|
@ -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.settings.logins
|
||||
package org.mozilla.fenix.settings.logins.fragment
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Activity.RESULT_OK
|
||||
|
@ -313,7 +313,8 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat(), AccountObserver {
|
|||
}
|
||||
|
||||
private fun navigateToAccountProblemFragment() {
|
||||
val directions = SavedLoginsAuthFragmentDirections.actionGlobalAccountProblemFragment()
|
||||
val directions =
|
||||
SavedLoginsAuthFragmentDirections.actionGlobalAccountProblemFragment()
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
|
|
@ -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.settings.logins
|
||||
package org.mozilla.fenix.settings.logins.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
|
@ -21,17 +21,9 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.android.synthetic.main.fragment_saved_logins.view.*
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.ObsoleteCoroutinesApi
|
||||
import mozilla.components.browser.menu.BrowserMenu
|
||||
import mozilla.components.concept.storage.Login
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
|
@ -41,17 +33,28 @@ import org.mozilla.fenix.ext.components
|
|||
import org.mozilla.fenix.ext.redirectToReAuth
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.settings.logins.LoginsAction
|
||||
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
|
||||
import org.mozilla.fenix.settings.logins.controller.LoginsListController
|
||||
import org.mozilla.fenix.settings.logins.LoginsListState
|
||||
import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor
|
||||
import org.mozilla.fenix.settings.logins.SavedLoginsSortingStrategyMenu
|
||||
import org.mozilla.fenix.settings.logins.view.SavedLoginsListView
|
||||
import org.mozilla.fenix.settings.logins.SortingStrategy
|
||||
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
||||
|
||||
@SuppressWarnings("TooManyFunctions")
|
||||
class SavedLoginsFragment : Fragment() {
|
||||
private lateinit var savedLoginsStore: LoginsFragmentStore
|
||||
private lateinit var savedLoginsView: SavedLoginsView
|
||||
private lateinit var savedLoginsListView: SavedLoginsListView
|
||||
private lateinit var savedLoginsInteractor: SavedLoginsInteractor
|
||||
private lateinit var dropDownMenuAnchorView: View
|
||||
private lateinit var sortingStrategyMenu: SavedLoginsSortingStrategyMenu
|
||||
private lateinit var sortingStrategyPopupMenu: BrowserMenu
|
||||
private lateinit var toolbarChildContainer: FrameLayout
|
||||
private lateinit var sortLoginsMenuRoot: ConstraintLayout
|
||||
private lateinit var loginsListController: LoginsListController
|
||||
private lateinit var savedLoginsStorageController: SavedLoginsStorageController
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
@ -81,21 +84,39 @@ class SavedLoginsFragment : Fragment() {
|
|||
filteredItems = listOf(),
|
||||
searchedForText = null,
|
||||
sortingStrategy = requireContext().settings().savedLoginsSortingStrategy,
|
||||
highlightedItem = requireContext().settings().savedLoginsMenuHighlightedItem
|
||||
highlightedItem = requireContext().settings().savedLoginsMenuHighlightedItem,
|
||||
duplicateLogins = listOf() // assume on load there are no dupes
|
||||
)
|
||||
)
|
||||
}
|
||||
val savedLoginsController: SavedLoginsController =
|
||||
SavedLoginsController(
|
||||
store = savedLoginsStore,
|
||||
navController = findNavController(),
|
||||
browserNavigator = ::openToBrowserAndLoad,
|
||||
settings = requireContext().settings(),
|
||||
metrics = requireContext().components.analytics.metrics
|
||||
|
||||
loginsListController =
|
||||
LoginsListController(
|
||||
loginsFragmentStore = savedLoginsStore,
|
||||
navController = findNavController(),
|
||||
browserNavigator = ::openToBrowserAndLoad,
|
||||
settings = requireContext().settings(),
|
||||
metrics = requireContext().components.analytics.metrics
|
||||
)
|
||||
savedLoginsInteractor = SavedLoginsInteractor(savedLoginsController)
|
||||
savedLoginsView = SavedLoginsView(view.savedLoginsLayout, savedLoginsInteractor)
|
||||
loadAndMapLogins()
|
||||
savedLoginsStorageController =
|
||||
SavedLoginsStorageController(
|
||||
context = requireContext(),
|
||||
viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
|
||||
navController = findNavController(),
|
||||
loginsFragmentStore = savedLoginsStore
|
||||
)
|
||||
|
||||
savedLoginsInteractor =
|
||||
SavedLoginsInteractor(
|
||||
loginsListController,
|
||||
savedLoginsStorageController
|
||||
)
|
||||
|
||||
savedLoginsListView = SavedLoginsListView(
|
||||
view.savedLoginsLayout,
|
||||
savedLoginsInteractor
|
||||
)
|
||||
savedLoginsInteractor.loadAndMapLogins()
|
||||
return view
|
||||
}
|
||||
|
||||
|
@ -105,7 +126,7 @@ class SavedLoginsFragment : Fragment() {
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
consumeFrom(savedLoginsStore) {
|
||||
sortingStrategyMenu.updateMenu(savedLoginsStore.state.highlightedItem)
|
||||
savedLoginsView.update(it)
|
||||
savedLoginsListView.update(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,7 +143,11 @@ class SavedLoginsFragment : Fragment() {
|
|||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
savedLoginsStore.dispatch(LoginsAction.FilterLogins(newText))
|
||||
savedLoginsStore.dispatch(
|
||||
LoginsAction.FilterLogins(
|
||||
newText
|
||||
)
|
||||
)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
@ -141,31 +166,11 @@ class SavedLoginsFragment : Fragment() {
|
|||
super.onPause()
|
||||
}
|
||||
|
||||
private fun openToBrowserAndLoad(searchTermOrURL: String, newTab: Boolean, from: BrowserDirection) {
|
||||
(activity as HomeActivity).openToBrowserAndLoad(searchTermOrURL, newTab, from)
|
||||
}
|
||||
|
||||
private fun loadAndMapLogins() {
|
||||
var deferredLogins: Deferred<List<Login>>? = null
|
||||
val fetchLoginsJob = viewLifecycleOwner.lifecycleScope.launch(IO) {
|
||||
deferredLogins = async {
|
||||
requireContext().components.core.passwordsStorage.list()
|
||||
}
|
||||
val logins = deferredLogins?.await()
|
||||
logins?.let {
|
||||
withContext(Main) {
|
||||
savedLoginsStore.dispatch(
|
||||
LoginsAction.UpdateLoginsList(logins.map { it.mapToSavedLogin() })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchLoginsJob.invokeOnCompletion {
|
||||
if (it is CancellationException) {
|
||||
deferredLogins?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun openToBrowserAndLoad(
|
||||
searchTermOrURL: String,
|
||||
newTab: Boolean,
|
||||
from: BrowserDirection
|
||||
) = (activity as HomeActivity).openToBrowserAndLoad(searchTermOrURL, newTab, from)
|
||||
|
||||
private fun initToolbar() {
|
||||
showToolbar(getString(R.string.preferences_passwords_saved_logins))
|
||||
|
@ -175,8 +180,12 @@ class SavedLoginsFragment : Fragment() {
|
|||
sortLoginsMenuRoot = inflateSortLoginsMenuRoot()
|
||||
dropDownMenuAnchorView = sortLoginsMenuRoot.findViewById(R.id.drop_down_menu_anchor_view)
|
||||
when (requireContext().settings().savedLoginsSortingStrategy) {
|
||||
is SortingStrategy.Alphabetically -> setupMenu(SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort)
|
||||
is SortingStrategy.LastUsed -> setupMenu(SavedLoginsSortingStrategyMenu.Item.LastUsedSort)
|
||||
is SortingStrategy.Alphabetically -> setupMenu(
|
||||
SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort
|
||||
)
|
||||
is SortingStrategy.LastUsed -> setupMenu(
|
||||
SavedLoginsSortingStrategyMenu.Item.LastUsedSort
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -210,21 +219,29 @@ class SavedLoginsFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun setupMenu(itemToHighlight: SavedLoginsSortingStrategyMenu.Item) {
|
||||
sortingStrategyMenu = SavedLoginsSortingStrategyMenu(requireContext(), itemToHighlight) {
|
||||
when (it) {
|
||||
SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort -> {
|
||||
savedLoginsInteractor.onSortingStrategyChanged(
|
||||
SortingStrategy.Alphabetically(requireContext().applicationContext)
|
||||
)
|
||||
}
|
||||
sortingStrategyMenu =
|
||||
SavedLoginsSortingStrategyMenu(
|
||||
requireContext(),
|
||||
itemToHighlight
|
||||
) {
|
||||
when (it) {
|
||||
SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort -> {
|
||||
savedLoginsInteractor.onSortingStrategyChanged(
|
||||
SortingStrategy.Alphabetically(
|
||||
requireContext().applicationContext
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
SavedLoginsSortingStrategyMenu.Item.LastUsedSort -> {
|
||||
savedLoginsInteractor.onSortingStrategyChanged(
|
||||
SortingStrategy.LastUsed(requireContext().applicationContext)
|
||||
)
|
||||
SavedLoginsSortingStrategyMenu.Item.LastUsedSort -> {
|
||||
savedLoginsInteractor.onSortingStrategyChanged(
|
||||
SortingStrategy.LastUsed(
|
||||
requireContext().applicationContext
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attachMenu()
|
||||
}
|
|
@ -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.settings.logins
|
||||
package org.mozilla.fenix.settings.logins.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.Preference
|
|
@ -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.settings.logins.interactor
|
||||
|
||||
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
||||
|
||||
/**
|
||||
* Interactor for the edit login screen
|
||||
*
|
||||
* @property savedLoginsController controller for the saved logins storage
|
||||
*/
|
||||
class EditLoginInteractor(
|
||||
private val savedLoginsController: SavedLoginsStorageController
|
||||
) {
|
||||
fun findPotentialDuplicates(loginId: String) {
|
||||
savedLoginsController.findPotentialDuplicates(loginId)
|
||||
}
|
||||
|
||||
fun onSaveLogin(loginId: String, usernameText: String, passwordText: String) {
|
||||
savedLoginsController.save(loginId, usernameText, passwordText)
|
||||
}
|
||||
}
|
|
@ -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.settings.logins.interactor
|
||||
|
||||
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
||||
|
||||
/**
|
||||
* Interactor for the login detail screen
|
||||
*
|
||||
* @property savedLoginsController controller for the saved logins storage
|
||||
*/
|
||||
class LoginDetailInteractor(
|
||||
private val savedLoginsController: SavedLoginsStorageController
|
||||
) {
|
||||
fun onFetchLoginList(loginId: String) {
|
||||
savedLoginsController.fetchLoginDetails(loginId)
|
||||
}
|
||||
|
||||
fun onDeleteLogin(loginId: String) {
|
||||
savedLoginsController.delete(loginId)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/* 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.settings.logins.interactor
|
||||
|
||||
import org.mozilla.fenix.settings.logins.SavedLogin
|
||||
import org.mozilla.fenix.settings.logins.SortingStrategy
|
||||
import org.mozilla.fenix.settings.logins.controller.LoginsListController
|
||||
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
||||
|
||||
/**
|
||||
* Interactor for the saved logins screen
|
||||
*
|
||||
* @param loginsListController [LoginsListController] which will be delegated for all
|
||||
* user interactions.
|
||||
* @param savedLoginsStorageController [SavedLoginsStorageController] which will be delegated
|
||||
* for all calls to the password storage component
|
||||
*/
|
||||
class SavedLoginsInteractor(
|
||||
private val loginsListController: LoginsListController,
|
||||
private val savedLoginsStorageController: SavedLoginsStorageController
|
||||
) {
|
||||
fun onItemClicked(item: SavedLogin) {
|
||||
loginsListController.handleItemClicked(item)
|
||||
}
|
||||
|
||||
fun onLearnMoreClicked() {
|
||||
loginsListController.handleLearnMoreClicked()
|
||||
}
|
||||
|
||||
fun onSortingStrategyChanged(sortingStrategy: SortingStrategy) {
|
||||
loginsListController.handleSort(sortingStrategy)
|
||||
}
|
||||
|
||||
fun loadAndMapLogins() {
|
||||
savedLoginsStorageController.handleLoadAndMapLogins()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/* 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.settings.logins.view
|
||||
|
||||
import android.text.InputType
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.fragment_edit_login.view.*
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
|
||||
/**
|
||||
* View that contains and configures the Edit Login screen
|
||||
*/
|
||||
@Suppress("ForbiddenComment")
|
||||
class EditLoginView(
|
||||
override val containerView: ViewGroup
|
||||
) : LayoutContainer {
|
||||
private val context = containerView.context
|
||||
|
||||
// TODO: create helper class for toggling passwords. https://github.com/mozilla-mobile/fenix/issues/12554
|
||||
fun showPassword() {
|
||||
val currText = containerView.passwordText?.text
|
||||
context.components.analytics.metrics.track(Event.ViewLoginPassword)
|
||||
containerView.passwordText?.inputType =
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||
containerView.revealPasswordButton?.setImageDrawable(
|
||||
AppCompatResources.getDrawable(context, R.drawable.mozac_ic_password_hide)
|
||||
)
|
||||
containerView.revealPasswordButton?.contentDescription =
|
||||
context.resources.getString(R.string.saved_login_hide_password)
|
||||
|
||||
// For the new type to take effect you need to reset the text to it's current edited version
|
||||
containerView.passwordText?.text = currText
|
||||
}
|
||||
|
||||
fun hidePassword() {
|
||||
val currText = containerView.passwordText?.text
|
||||
containerView.passwordText?.inputType =
|
||||
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
containerView.revealPasswordButton?.setImageDrawable(
|
||||
AppCompatResources.getDrawable(context, R.drawable.mozac_ic_password_reveal)
|
||||
)
|
||||
containerView.revealPasswordButton?.contentDescription =
|
||||
context.getString(R.string.saved_login_reveal_password)
|
||||
|
||||
// For the new type to take effect you need to reset the text to it's current edited version
|
||||
containerView.passwordText?.text = currText
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/* 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.settings.logins.view
|
||||
|
||||
import android.text.InputType
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.fragment_login_detail.*
|
||||
import kotlinx.android.synthetic.main.fragment_login_detail.view.*
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.settings.logins.LoginsListState
|
||||
|
||||
/**
|
||||
* View that contains and configures the Login Details
|
||||
*/
|
||||
@Suppress("ForbiddenComment")
|
||||
class LoginDetailView(override val containerView: ViewGroup) : LayoutContainer {
|
||||
private val context = containerView.context
|
||||
|
||||
fun update(login: LoginsListState) {
|
||||
webAddressText.text = login.currentItem?.origin
|
||||
usernameText.text = login.currentItem?.username
|
||||
passwordText.text = login.currentItem?.password
|
||||
}
|
||||
|
||||
fun togglePasswordReveal(show: Boolean) {
|
||||
if (show) showPassword() else { hidePassword() }
|
||||
}
|
||||
|
||||
// TODO: create helper class for toggling passwords. https://github.com/mozilla-mobile/fenix/issues/12554
|
||||
fun showPassword() {
|
||||
if (passwordText.inputType == InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT) {
|
||||
context?.components?.analytics?.metrics?.track(Event.ViewLoginPassword)
|
||||
passwordText.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||
revealPasswordButton.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
context.resources,
|
||||
R.drawable.mozac_ic_password_hide, null
|
||||
)
|
||||
)
|
||||
revealPasswordButton.contentDescription =
|
||||
context.resources.getString(R.string.saved_login_hide_password)
|
||||
}
|
||||
// For the new type to take effect you need to reset the text
|
||||
passwordText.text = containerView.passwordText.editableText
|
||||
}
|
||||
|
||||
fun hidePassword() {
|
||||
passwordText.inputType =
|
||||
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
revealPasswordButton.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(context.resources,
|
||||
R.drawable.mozac_ic_password_reveal, null)
|
||||
)
|
||||
revealPasswordButton.contentDescription =
|
||||
context.getString(R.string.saved_login_reveal_password)
|
||||
// For the new type to take effect you need to reset the text
|
||||
passwordText.text = containerView.passwordText.editableText
|
||||
}
|
||||
}
|
|
@ -2,13 +2,15 @@
|
|||
* 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.settings.logins
|
||||
package org.mozilla.fenix.settings.logins.view
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.settings.logins.SavedLogin
|
||||
import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor
|
||||
|
||||
class LoginsAdapter(
|
||||
private val interactor: SavedLoginsInteractor
|
|
@ -2,13 +2,15 @@
|
|||
* 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.settings.logins
|
||||
package org.mozilla.fenix.settings.logins.view
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.logins_item.view.*
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.loadIntoView
|
||||
import org.mozilla.fenix.settings.logins.SavedLogin
|
||||
import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor
|
||||
|
||||
class LoginsListViewHolder(
|
||||
private val view: View,
|
|
@ -0,0 +1,67 @@
|
|||
/* 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.settings.logins.view
|
||||
|
||||
import android.text.method.LinkMovementMethod
|
||||
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_saved_logins.view.*
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.settings.logins.LoginsListState
|
||||
import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor
|
||||
import org.mozilla.fenix.ext.addUnderline
|
||||
|
||||
/**
|
||||
* View that contains and configures the Saved Logins List
|
||||
*/
|
||||
class SavedLoginsListView(
|
||||
override val containerView: ViewGroup,
|
||||
val interactor: SavedLoginsInteractor
|
||||
) : LayoutContainer {
|
||||
|
||||
val view: FrameLayout = LayoutInflater.from(containerView.context)
|
||||
.inflate(R.layout.component_saved_logins, containerView, true)
|
||||
.findViewById(R.id.saved_logins_wrapper)
|
||||
|
||||
private val loginsAdapter = LoginsAdapter(interactor)
|
||||
|
||||
init {
|
||||
view.saved_logins_list.apply {
|
||||
adapter = loginsAdapter
|
||||
layoutManager = LinearLayoutManager(containerView.context)
|
||||
itemAnimator = null
|
||||
}
|
||||
|
||||
with(view.saved_passwords_empty_learn_more) {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
addUnderline()
|
||||
setOnClickListener { interactor.onLearnMoreClicked() }
|
||||
}
|
||||
|
||||
with(view.saved_passwords_empty_message) {
|
||||
val appName = context.getString(R.string.app_name)
|
||||
text = String.format(
|
||||
context.getString(
|
||||
R.string.preferences_passwords_saved_logins_description_empty_text
|
||||
), appName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun update(state: LoginsListState) {
|
||||
if (state.isLoading) {
|
||||
view.progress_bar.isVisible = true
|
||||
} else {
|
||||
view.progress_bar.isVisible = false
|
||||
view.saved_logins_list.isVisible = state.loginList.isNotEmpty()
|
||||
view.saved_passwords_empty_view.isVisible = state.loginList.isEmpty()
|
||||
}
|
||||
loginsAdapter.submitList(state.filteredItems)
|
||||
}
|
||||
}
|
|
@ -16,9 +16,9 @@ import org.mozilla.fenix.R
|
|||
import org.mozilla.fenix.ext.requireComponents
|
||||
|
||||
/**
|
||||
* Dialog displayed the first time the user navigates to an installable web app.
|
||||
* Dialog displayed the third time the user navigates to an installable web app.
|
||||
*/
|
||||
class FirstTimePwaFragment : DialogFragment() {
|
||||
class PwaOnboardingDialogFragment : DialogFragment() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_TITLE, R.style.CreateShortcutDialogStyle)
|
||||
|
@ -28,7 +28,7 @@ class FirstTimePwaFragment : DialogFragment() {
|
|||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? = inflater.inflate(R.layout.fragment_pwa_first_time, container, false)
|
||||
): View? = inflater.inflate(R.layout.fragment_pwa_onboarding, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
|
@ -14,20 +14,23 @@ import org.mozilla.fenix.ext.nav
|
|||
import org.mozilla.fenix.utils.Settings
|
||||
|
||||
/**
|
||||
* Displays the [FirstTimePwaFragment] info dialog when a PWA is first opened in the browser.
|
||||
* Displays the [PwaOnboardingDialogFragment] info dialog when a PWA is opened in the browser for the third time.
|
||||
*/
|
||||
class FirstTimePwaObserver(
|
||||
class PwaOnboardingObserver(
|
||||
private val navController: NavController,
|
||||
private val settings: Settings,
|
||||
private val webAppUseCases: WebAppUseCases
|
||||
) : Session.Observer {
|
||||
|
||||
override fun onWebAppManifestChanged(session: Session, manifest: WebAppManifest?) {
|
||||
if (webAppUseCases.isInstallable() && settings.shouldShowFirstTimePwaFragment) {
|
||||
val directions = BrowserFragmentDirections.actionBrowserFragmentToFirstTimePwaFragment()
|
||||
navController.nav(R.id.browserFragment, directions)
|
||||
|
||||
settings.userKnowsAboutPWAs = true
|
||||
if (webAppUseCases.isInstallable() && !settings.userKnowsAboutPwas) {
|
||||
settings.incrementVisitedInstallableCount()
|
||||
if (settings.shouldShowPwaOnboarding) {
|
||||
val directions =
|
||||
BrowserFragmentDirections.actionBrowserFragmentToPwaOnboardingDialogFragment()
|
||||
navController.nav(R.id.browserFragment, directions)
|
||||
settings.userKnowsAboutPwas = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
|
@ -25,6 +26,7 @@ import mozilla.components.browser.session.SessionManager
|
|||
import mozilla.components.browser.state.selector.normalTabs
|
||||
import mozilla.components.browser.state.selector.privateTabs
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.feature.tab.collections.TabCollection
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import mozilla.components.feature.tabs.tabstray.TabsFeature
|
||||
|
@ -37,6 +39,7 @@ import org.mozilla.fenix.components.TabCollectionStorage
|
|||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.utils.allowUndo
|
||||
|
||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||
|
@ -48,7 +51,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
|||
|
||||
private val snackbarAnchor: View?
|
||||
get() = if (tabTrayView.fabView.new_tab_button.isVisible) tabTrayView.fabView.new_tab_button
|
||||
else null
|
||||
else null
|
||||
|
||||
private val collectionStorageObserver = object : TabCollectionStorage.Observer {
|
||||
override fun onCollectionCreated(title: String, sessions: List<Session>) {
|
||||
|
@ -131,7 +134,13 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
|||
startingInLandscape = requireContext().resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE,
|
||||
lifecycleScope = viewLifecycleOwner.lifecycleScope
|
||||
) { tabsFeature.get()?.filterTabs(it) }
|
||||
) { private ->
|
||||
val filter: (TabSessionState) -> Boolean = { state -> private == state.content.private }
|
||||
|
||||
tabsFeature.get()?.filterTabs(filter)
|
||||
|
||||
setSecureFlagsIfNeeded(private)
|
||||
}
|
||||
|
||||
tabsFeature.set(
|
||||
TabsFeature(
|
||||
|
@ -171,6 +180,14 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun setSecureFlagsIfNeeded(private: Boolean) {
|
||||
if (private && context?.settings()?.allowScreenshotsInPrivateMode == false) {
|
||||
dialog?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
} else if (!(activity as HomeActivity).browsingModeManager.mode.isPrivate) {
|
||||
dialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUndoSnackbarForTab(sessionId: String) {
|
||||
val sessionManager = view?.context?.components?.core?.sessionManager
|
||||
val snapshot = sessionManager
|
||||
|
|
|
@ -27,7 +27,6 @@ import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
|
|||
import mozilla.components.browser.state.selector.normalTabs
|
||||
import mozilla.components.browser.state.selector.privateTabs
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.tabstray.BrowserTabsTray
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
|
@ -44,7 +43,7 @@ class TabTrayView(
|
|||
isPrivate: Boolean,
|
||||
startingInLandscape: Boolean,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
private val filterTabs: ((TabSessionState) -> Boolean) -> Unit
|
||||
private val filterTabs: (Boolean) -> Unit
|
||||
) : LayoutContainer, TabLayout.OnTabSelectedListener {
|
||||
val fabView = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_tabstray_fab, container, true)
|
||||
|
@ -204,14 +203,8 @@ class TabTrayView(
|
|||
}
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
// We need a better way to determine which tab was selected.
|
||||
val filter: (TabSessionState) -> Boolean = when (tab?.position) {
|
||||
1 -> { state -> state.content.private }
|
||||
else -> { state -> !state.content.private }
|
||||
}
|
||||
|
||||
toggleFabText(isPrivateModeSelected)
|
||||
filterTabs.invoke(filter)
|
||||
filterTabs.invoke(isPrivateModeSelected)
|
||||
|
||||
updateState(view.context.components.core.store.state)
|
||||
scrollToTab(view.context.components.core.store.state.selectedTabId)
|
||||
|
|
|
@ -33,7 +33,7 @@ import org.mozilla.fenix.ext.components
|
|||
import org.mozilla.fenix.ext.getPreferenceKey
|
||||
import org.mozilla.fenix.settings.PhoneFeature
|
||||
import org.mozilla.fenix.settings.deletebrowsingdata.DeleteBrowsingDataOnQuitType
|
||||
import org.mozilla.fenix.settings.logins.SavedLoginsFragment
|
||||
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsFragment
|
||||
import org.mozilla.fenix.settings.logins.SavedLoginsSortingStrategyMenu
|
||||
import org.mozilla.fenix.settings.logins.SortingStrategy
|
||||
import org.mozilla.fenix.settings.registerOnSharedPreferenceChangeListener
|
||||
|
@ -53,6 +53,7 @@ class Settings private constructor(
|
|||
const val showLoginsSecureWarningSyncMaxCount = 1
|
||||
const val showLoginsSecureWarningMaxCount = 1
|
||||
const val trackingProtectionOnboardingMaximumCount = 1
|
||||
const val pwaVisitsToShowPromptMaxCount = 3
|
||||
const val FENIX_PREFERENCES = "fenix_preferences"
|
||||
|
||||
private const val showSearchWidgetCFRMaxCount = 3
|
||||
|
@ -146,9 +147,18 @@ class Settings private constructor(
|
|||
|
||||
// If any of the prefs have been modified, quit displaying the fenix moved tip
|
||||
fun shouldDisplayFenixMovingTip(): Boolean =
|
||||
preferences.getBoolean(appContext.getString(R.string.pref_key_migrating_from_fenix_nightly_tip), true) &&
|
||||
preferences.getBoolean(appContext.getString(R.string.pref_key_migrating_from_firefox_nightly_tip), true) &&
|
||||
preferences.getBoolean(appContext.getString(R.string.pref_key_migrating_from_fenix_tip), true)
|
||||
preferences.getBoolean(
|
||||
appContext.getString(R.string.pref_key_migrating_from_fenix_nightly_tip),
|
||||
true
|
||||
) &&
|
||||
preferences.getBoolean(
|
||||
appContext.getString(R.string.pref_key_migrating_from_firefox_nightly_tip),
|
||||
true
|
||||
) &&
|
||||
preferences.getBoolean(
|
||||
appContext.getString(R.string.pref_key_migrating_from_fenix_tip),
|
||||
true
|
||||
)
|
||||
|
||||
private val activeSearchCount by intPreference(
|
||||
appContext.getPreferenceKey(R.string.pref_key_search_count),
|
||||
|
@ -167,9 +177,9 @@ class Settings private constructor(
|
|||
|
||||
fun shouldDisplaySearchWidgetCFR(): Boolean =
|
||||
isActiveSearcher &&
|
||||
searchWidgetCFRDismissCount < showSearchWidgetCFRMaxCount &&
|
||||
!searchWidgetInstalled &&
|
||||
!searchWidgetCFRManuallyDismissed
|
||||
searchWidgetCFRDismissCount < showSearchWidgetCFRMaxCount &&
|
||||
!searchWidgetInstalled &&
|
||||
!searchWidgetCFRManuallyDismissed
|
||||
|
||||
private val searchWidgetCFRDisplayCount by intPreference(
|
||||
appContext.getPreferenceKey(R.string.pref_key_search_widget_cfr_display_count),
|
||||
|
@ -236,10 +246,10 @@ class Settings private constructor(
|
|||
|
||||
val isCrashReportingEnabled: Boolean
|
||||
get() = isCrashReportEnabledInBuild &&
|
||||
preferences.getBoolean(
|
||||
appContext.getPreferenceKey(R.string.pref_key_crash_reporter),
|
||||
true
|
||||
)
|
||||
preferences.getBoolean(
|
||||
appContext.getPreferenceKey(R.string.pref_key_crash_reporter),
|
||||
true
|
||||
)
|
||||
|
||||
val isRemoteDebuggingEnabled by booleanPreference(
|
||||
appContext.getPreferenceKey(R.string.pref_key_remote_debugging),
|
||||
|
@ -267,7 +277,7 @@ class Settings private constructor(
|
|||
val shouldShowTrackingProtectionOnboarding: Boolean
|
||||
get() = !isOverrideTPPopupsForPerformanceTest &&
|
||||
(trackingProtectionOnboardingCount < trackingProtectionOnboardingMaximumCount &&
|
||||
!trackingProtectionOnboardingShownThisSession)
|
||||
!trackingProtectionOnboardingShownThisSession)
|
||||
|
||||
var showSecretDebugMenuThisSession = false
|
||||
|
||||
|
@ -418,14 +428,14 @@ class Settings private constructor(
|
|||
BrowsingMode.Normal
|
||||
}
|
||||
}
|
||||
|
||||
set(value) {
|
||||
val lastKnownModeWasPrivate = (value == BrowsingMode.Private)
|
||||
|
||||
preferences.edit()
|
||||
.putBoolean(
|
||||
appContext.getPreferenceKey(R.string.pref_key_last_known_mode_private),
|
||||
lastKnownModeWasPrivate)
|
||||
appContext.getPreferenceKey(R.string.pref_key_last_known_mode_private),
|
||||
lastKnownModeWasPrivate
|
||||
)
|
||||
.apply()
|
||||
|
||||
field = value
|
||||
|
@ -495,7 +505,9 @@ class Settings private constructor(
|
|||
}
|
||||
|
||||
val accessibilityServicesEnabled: Boolean
|
||||
get() { return touchExplorationIsEnabled || switchServiceIsEnabled }
|
||||
get() {
|
||||
return touchExplorationIsEnabled || switchServiceIsEnabled
|
||||
}
|
||||
|
||||
val toolbarSettingString: String
|
||||
get() = when {
|
||||
|
@ -569,22 +581,41 @@ class Settings private constructor(
|
|||
default = false
|
||||
)
|
||||
|
||||
val shouldShowFirstTimePwaFragment: Boolean
|
||||
fun incrementVisitedInstallableCount() {
|
||||
preferences.edit().putInt(
|
||||
appContext.getPreferenceKey(R.string.pref_key_install_pwa_visits),
|
||||
pwaInstallableVisitCount + 1
|
||||
).apply()
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = PRIVATE)
|
||||
internal val pwaInstallableVisitCount by intPreference(
|
||||
appContext.getPreferenceKey(R.string.pref_key_install_pwa_visits),
|
||||
default = 0
|
||||
)
|
||||
|
||||
private val userNeedsToVisitInstallableSites: Boolean
|
||||
get() = pwaInstallableVisitCount < pwaVisitsToShowPromptMaxCount
|
||||
|
||||
val shouldShowPwaOnboarding: Boolean
|
||||
get() {
|
||||
// We only want to show this on the 3rd time a user visits a site
|
||||
if (userNeedsToVisitInstallableSites) return false
|
||||
|
||||
// ShortcutManager::pinnedShortcuts is only available on Oreo+
|
||||
if (!userKnowsAboutPWAs && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val alreadyHavePWaInstalled =
|
||||
if (!userKnowsAboutPwas && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val alreadyHavePwaInstalled =
|
||||
appContext.getSystemService(ShortcutManager::class.java)
|
||||
.pinnedShortcuts.size > 0
|
||||
|
||||
// Users know about PWAs onboarding if they already have PWAs installed.
|
||||
userKnowsAboutPWAs = alreadyHavePWaInstalled
|
||||
userKnowsAboutPwas = alreadyHavePwaInstalled
|
||||
}
|
||||
// Show dialog only if user does not know abut PWAs
|
||||
return !userKnowsAboutPWAs
|
||||
return !userKnowsAboutPwas
|
||||
}
|
||||
|
||||
var userKnowsAboutPWAs by booleanPreference(
|
||||
var userKnowsAboutPwas by booleanPreference(
|
||||
appContext.getPreferenceKey(R.string.pref_key_user_knows_about_pwa),
|
||||
default = false
|
||||
)
|
||||
|
@ -809,8 +840,12 @@ class Settings private constructor(
|
|||
var savedLoginsSortingStrategy: SortingStrategy
|
||||
get() {
|
||||
return when (savedLoginsSortingStrategyString) {
|
||||
SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY -> SortingStrategy.Alphabetically(appContext)
|
||||
SavedLoginsFragment.SORTING_STRATEGY_LAST_USED -> SortingStrategy.LastUsed(appContext)
|
||||
SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY -> SortingStrategy.Alphabetically(
|
||||
appContext
|
||||
)
|
||||
SavedLoginsFragment.SORTING_STRATEGY_LAST_USED -> SortingStrategy.LastUsed(
|
||||
appContext
|
||||
)
|
||||
else -> SortingStrategy.Alphabetically(appContext)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<?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/. -->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_enabled="true" android:color="?primaryText"/>
|
||||
<item android:state_enabled="false" android:color="?disabled" />
|
||||
</selector>
|
|
@ -6,5 +6,5 @@
|
|||
<item android:state_enabled="true"
|
||||
android:color="?primaryText" />
|
||||
<item android:state_enabled="false"
|
||||
android:color="@android:color/transparent" />
|
||||
android:color="?disabled" />
|
||||
</selector>
|
|
@ -2,24 +2,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/. -->
|
||||
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/permissions_blocked_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="16dp"
|
||||
android:paddingStart="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingEnd="@dimen/radio_button_preference_horizontal"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/permissions_blocked_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="16dp"
|
||||
android:paddingStart="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingEnd="@dimen/radio_button_preference_horizontal"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:layout_marginStart="@dimen/site_permissions_exceptions_item_height"
|
||||
android:text="@string/phone_feature_blocked_by_android"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
|
@ -27,21 +27,24 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSmall"
|
||||
android:layout_marginStart="@dimen/site_permissions_exceptions_item_height"
|
||||
android:text="@string/phone_feature_blocked_intro"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSmall"
|
||||
android:layout_marginStart="@dimen/site_permissions_exceptions_item_height"
|
||||
android:text="@string/phone_feature_blocked_step_settings"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/blocked_by_android_permissions_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSmall"
|
||||
android:layout_marginStart="@dimen/site_permissions_exceptions_item_height"
|
||||
tools:text="@string/phone_feature_blocked_step_permissions"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
|
@ -50,7 +53,8 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSmall"
|
||||
tools:text="@string/phone_feature_blocked_step_feature"/>
|
||||
android:layout_marginStart="@dimen/site_permissions_exceptions_item_height"
|
||||
tools:text="@string/phone_feature_blocked_step_feature" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/settings_button"
|
||||
|
|
|
@ -134,12 +134,13 @@
|
|||
android:id="@+id/clearUsernameTextButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginTop="3dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:background="@null"
|
||||
android:contentDescription="@string/saved_login_copy_username"
|
||||
app:tint="@color/saved_login_clear_edit_text_tint"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="@id/inputLayoutUsername"
|
||||
app:layout_constraintTop_toTopOf="@id/inputLayoutUsername"
|
||||
app:srcCompat="@drawable/ic_clear" />
|
||||
|
||||
<TextView
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
android:visibility="gone"
|
||||
android:orientation="vertical">
|
||||
|
||||
<Switch
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/enable_switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -34,7 +34,7 @@
|
|||
android:textColor="?primaryText"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<Switch
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/allow_in_private_browsing_switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -93,7 +93,7 @@
|
|||
android:textSize="16sp"
|
||||
app:drawableStartCompat="@drawable/ic_permission" />
|
||||
|
||||
<Button
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/remove_add_on"
|
||||
style="@style/DestructiveButton"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
|
|
|
@ -6,18 +6,18 @@
|
|||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="@dimen/radio_button_preference_vertical">
|
||||
|
||||
<RadioGroup
|
||||
<RadioGroup
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RadioButton
|
||||
<RadioButton
|
||||
android:id="@+id/ask_to_allow_radio"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -26,13 +26,13 @@
|
|||
android:background="?android:attr/selectableItemBackground"
|
||||
android:button="@null"
|
||||
app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle"
|
||||
android:drawablePadding="@dimen/radio_button_preference_drawable_padding"
|
||||
android:drawablePadding="@dimen/preference_seek_bar_padding"
|
||||
android:paddingTop="@dimen/radio_button_preference_vertical"
|
||||
android:paddingStart="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingEnd="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingBottom="@dimen/radio_button_preference_vertical"/>
|
||||
android:paddingBottom="@dimen/radio_button_preference_vertical" />
|
||||
|
||||
<RadioButton
|
||||
<RadioButton
|
||||
android:id="@+id/block_radio"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/radio_button_preference_height"
|
||||
|
@ -41,15 +41,15 @@
|
|||
android:background="?android:attr/selectableItemBackground"
|
||||
android:button="@null"
|
||||
app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle"
|
||||
android:drawablePadding="@dimen/radio_button_preference_drawable_padding"
|
||||
android:drawablePadding="@dimen/preference_seek_bar_padding"
|
||||
android:paddingTop="@dimen/radio_button_preference_vertical"
|
||||
android:paddingStart="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingEnd="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingBottom="@dimen/radio_button_preference_vertical"/>
|
||||
</RadioGroup>
|
||||
android:paddingBottom="@dimen/radio_button_preference_vertical" />
|
||||
</RadioGroup>
|
||||
|
||||
<include layout="@layout/layout_clear_permission_button"/>
|
||||
<include layout="@layout/component_permissions_blocked_by_android"/>
|
||||
<include layout="@layout/layout_clear_permission_button" />
|
||||
<include layout="@layout/component_permissions_blocked_by_android" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
|
@ -7,76 +7,77 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="@dimen/radio_button_preference_vertical">
|
||||
|
||||
<RadioGroup
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="@dimen/radio_button_preference_vertical">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/ask_to_allow_radio"
|
||||
<RadioGroup
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:button="@null"
|
||||
android:drawablePadding="@dimen/radio_button_preference_drawable_padding"
|
||||
android:paddingStart="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingTop="@dimen/radio_button_preference_vertical"
|
||||
android:paddingEnd="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingBottom="@dimen/radio_button_preference_vertical"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle"
|
||||
tools:text="@string/preference_option_phone_feature_ask_to_allow" />
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/block_radio"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:button="@null"
|
||||
android:drawablePadding="@dimen/radio_button_preference_drawable_padding"
|
||||
android:paddingStart="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingTop="@dimen/radio_button_preference_vertical"
|
||||
android:paddingEnd="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingBottom="@dimen/radio_button_preference_vertical"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle"
|
||||
tools:text="@string/preference_option_phone_feature_blocked" />
|
||||
<RadioButton
|
||||
android:id="@+id/ask_to_allow_radio"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:button="@null"
|
||||
android:drawablePadding="@dimen/preference_seek_bar_padding"
|
||||
android:paddingStart="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingTop="@dimen/radio_button_preference_vertical"
|
||||
android:paddingEnd="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingBottom="@dimen/radio_button_preference_vertical"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle"
|
||||
tools:text="@string/preference_option_phone_feature_ask_to_allow" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/third_radio"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:button="@null"
|
||||
android:drawablePadding="@dimen/radio_button_preference_drawable_padding"
|
||||
android:paddingStart="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingTop="@dimen/radio_button_preference_vertical"
|
||||
android:paddingEnd="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingBottom="@dimen/radio_button_preference_vertical"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle" />
|
||||
<RadioButton
|
||||
android:id="@+id/block_radio"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:button="@null"
|
||||
android:drawablePadding="@dimen/preference_seek_bar_padding"
|
||||
android:paddingStart="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingTop="@dimen/radio_button_preference_vertical"
|
||||
android:paddingEnd="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingBottom="@dimen/radio_button_preference_vertical"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle"
|
||||
tools:text="@string/preference_option_phone_feature_blocked" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/fourth_radio"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:button="@null"
|
||||
android:drawablePadding="@dimen/radio_button_preference_drawable_padding"
|
||||
android:paddingStart="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingTop="@dimen/radio_button_preference_vertical"
|
||||
android:paddingEnd="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingBottom="@dimen/radio_button_preference_vertical"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle" />
|
||||
</RadioGroup>
|
||||
<RadioButton
|
||||
android:id="@+id/third_radio"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:button="@null"
|
||||
android:drawablePadding="@dimen/preference_seek_bar_padding"
|
||||
android:paddingStart="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingTop="@dimen/radio_button_preference_vertical"
|
||||
android:paddingEnd="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingBottom="@dimen/radio_button_preference_vertical"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle" />
|
||||
|
||||
<include layout="@layout/component_permissions_blocked_by_android"/>
|
||||
<RadioButton
|
||||
android:id="@+id/fourth_radio"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:button="@null"
|
||||
android:drawablePadding="@dimen/preference_seek_bar_padding"
|
||||
android:paddingStart="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingTop="@dimen/radio_button_preference_vertical"
|
||||
android:paddingEnd="@dimen/radio_button_preference_horizontal"
|
||||
android:paddingBottom="@dimen/radio_button_preference_vertical"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle" />
|
||||
</RadioGroup>
|
||||
|
||||
</LinearLayout>
|
||||
<include layout="@layout/component_permissions_blocked_by_android"/>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
|
@ -10,7 +10,7 @@
|
|||
android:layout_height="match_parent"
|
||||
android:background="@drawable/scrim_background"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="org.mozilla.fenix.shortcut.FirstTimePwaFragment">
|
||||
tools:context="org.mozilla.fenix.shortcut.PwaOnboardingDialogFragment">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
|
@ -7,4 +7,4 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context="org.mozilla.fenix.settings.logins.SavedLoginsFragment" />
|
||||
tools:context="org.mozilla.fenix.settings.logins.fragment.SavedLoginsFragment" />
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<item
|
||||
android:id="@+id/save_login_button"
|
||||
android:icon="@drawable/mozac_ic_check"
|
||||
app:iconTint="?primaryText"
|
||||
app:iconTint="@color/save_enabled_ic_color"
|
||||
android:title="@string/save_changes_to_login"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
|
@ -221,8 +221,8 @@
|
|||
android:id="@+id/action_browserFragment_to_createShortcutFragment"
|
||||
app:destination="@id/createShortcutFragment" />
|
||||
<action
|
||||
android:id="@+id/action_browserFragment_to_firstTimePwaFragment"
|
||||
app:destination="@id/firstTimePwaFragment" />
|
||||
android:id="@+id/action_browserFragment_to_pwaOnboardingDialogFragment"
|
||||
app:destination="@id/pwaOnboardingDialogFragment" />
|
||||
<action
|
||||
android:id="@+id/action_browserFragment_to_quickSettingsSheetDialogFragment"
|
||||
app:destination="@id/quickSettingsSheetDialogFragment" />
|
||||
|
@ -321,7 +321,7 @@
|
|||
|
||||
<fragment
|
||||
android:id="@+id/savedLoginsAuthFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.SavedLoginsAuthFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragment"
|
||||
android:label="@string/preferences_passwords_logins_and_passwords">
|
||||
<action
|
||||
android:id="@+id/action_savedLoginsAuthFragment_to_loginsListFragment"
|
||||
|
@ -355,7 +355,7 @@
|
|||
|
||||
<fragment
|
||||
android:id="@+id/savedLoginsFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.SavedLoginsFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.fragment.SavedLoginsFragment"
|
||||
tools:layout="@layout/fragment_saved_logins">
|
||||
<action
|
||||
android:id="@+id/action_savedLoginsFragment_to_loginDetailFragment"
|
||||
|
@ -381,7 +381,7 @@
|
|||
|
||||
<fragment
|
||||
android:id="@+id/loginDetailFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.LoginDetailFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.fragment.LoginDetailFragment"
|
||||
tools:layout="@layout/fragment_login_detail">
|
||||
<argument
|
||||
android:name="savedLoginId"
|
||||
|
@ -396,7 +396,7 @@
|
|||
|
||||
<fragment
|
||||
android:id="@+id/editLoginFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.EditLoginFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.fragment.EditLoginFragment"
|
||||
android:label="@string/edit">
|
||||
<argument
|
||||
android:name="savedLoginItem"
|
||||
|
@ -680,10 +680,9 @@
|
|||
android:name="org.mozilla.fenix.shortcut.CreateShortcutFragment"
|
||||
tools:layout="@layout/fragment_create_shortcut" />
|
||||
<dialog
|
||||
android:id="@+id/firstTimePwaFragment"
|
||||
android:name="org.mozilla.fenix.shortcut.FirstTimePwaFragment"
|
||||
android:label="fragment_pwa_first_time"
|
||||
tools:layout="@layout/fragment_pwa_first_time" />
|
||||
android:id="@+id/pwaOnboardingDialogFragment"
|
||||
android:name="org.mozilla.fenix.shortcut.PwaOnboardingDialogFragment"
|
||||
tools:layout="@layout/fragment_pwa_onboarding" />
|
||||
|
||||
<dialog
|
||||
android:id="@+id/shareFragment"
|
||||
|
@ -790,7 +789,7 @@
|
|||
android:name="org.mozilla.fenix.settings.advanced.LocaleSettingsFragment" />
|
||||
<fragment
|
||||
android:id="@+id/saveLoginSettingFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.SavedLoginsSettingFragment"
|
||||
android:name="org.mozilla.fenix.settings.logins.fragment.SavedLoginsSettingFragment"
|
||||
android:label="SaveLoginSettingFragment" />
|
||||
<fragment
|
||||
android:id="@+id/addonsManagementFragment"
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
<string name="pref_key_private_mode_opened" translatable="false">pref_key_private_mode_opened</string>
|
||||
<string name="pref_key_open_in_app_opened" translatable="false">pref_key_open_in_app_opened</string>
|
||||
<string name="pref_key_install_pwa_opened" translatable="false">pref_key_install_pwa_opened</string>
|
||||
<string name="pref_key_install_pwa_visits" translatable="false">pref_key_install_pwa_visits</string>
|
||||
|
||||
<!-- Data Choices -->
|
||||
<string name="pref_key_telemetry" translatable="false">pref_key_telemetry</string>
|
||||
|
|
|
@ -566,6 +566,8 @@
|
|||
<string name="bookmark_select_folder">Select folder</string>
|
||||
<!-- Confirmation message for a dialog confirming if the user wants to delete the selected folder -->
|
||||
<string name="bookmark_delete_folder_confirmation_dialog">Are you sure you want to delete this folder?</string>
|
||||
<!-- Confirmation message for a dialog confirming if the user wants to delete multiple items including folders. Parameter will be replaced by app name. -->
|
||||
<string name="bookmark_delete_multiple_folders_confirmation_dialog">%s will delete the selected items.</string>
|
||||
<!-- Snackbar title shown after a folder has been deleted. This first parameter is the name of the deleted folder -->
|
||||
<string name="bookmark_delete_folder_snackbar">Deleted %1$s</string>
|
||||
<!-- Screen title for adding a bookmarks folder -->
|
||||
|
@ -620,8 +622,10 @@
|
|||
<!-- Bookmark snackbar message on deletion
|
||||
The first parameter is the host part of the URL of the bookmark deleted, if any -->
|
||||
<string name="bookmark_deletion_snackbar_message">Deleted %1$s</string>
|
||||
<!-- Bookmark snackbar message on deleting multiple bookmarks -->
|
||||
<!-- Bookmark snackbar message on deleting multiple bookmarks not including folders-->
|
||||
<string name="bookmark_deletion_multiple_snackbar_message_2">Bookmarks deleted</string>
|
||||
<!-- Bookmark snackbar message on deleting multiple bookmarks including folders-->
|
||||
<string name="bookmark_deletion_multiple_snackbar_message_3">Deleting selected folders</string>
|
||||
<!-- Bookmark undo button for deletion snackbar action -->
|
||||
<string name="bookmark_undo_deletion">UNDO</string>
|
||||
|
||||
|
|
|
@ -2,20 +2,16 @@
|
|||
<!-- 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"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<androidx.preference.Preference
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<Preference
|
||||
android:key="@string/pref_key_add_private_browsing_shortcut"
|
||||
android:title="@string/preferences_add_private_browsing_shortcut"
|
||||
app:iconSpaceReserved="false" />
|
||||
android:title="@string/preferences_add_private_browsing_shortcut" />
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="@string/pref_key_open_links_in_a_private_tab"
|
||||
android:title="@string/preferences_open_links_in_a_private_tab"
|
||||
app:iconSpaceReserved="false" />
|
||||
android:title="@string/preferences_open_links_in_a_private_tab" />
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="@string/pref_key_allow_screenshots_in_private_mode"
|
||||
android:title="@string/preferences_allow_screenshots_in_private_mode"
|
||||
app:iconSpaceReserved="false" />
|
||||
android:title="@string/preferences_allow_screenshots_in_private_mode" />
|
||||
</PreferenceScreen>
|
||||
|
|
|
@ -12,9 +12,11 @@ import io.mockk.coVerify
|
|||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import mozilla.components.feature.intent.processing.IntentProcessor
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
|
@ -39,6 +41,7 @@ class IntentReceiverActivityTest {
|
|||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockkStatic("org.mozilla.fenix.ext.ContextKt")
|
||||
settings = mockk()
|
||||
intentProcessors = mockk()
|
||||
|
||||
|
@ -54,6 +57,11 @@ class IntentReceiverActivityTest {
|
|||
coEvery { intentProcessors.intentProcessor.process(any()) } returns true
|
||||
}
|
||||
|
||||
@After
|
||||
fun teardown() {
|
||||
unmockkStatic("org.mozilla.fenix.ext.ContextKt")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process intent with flag launched from history`() = runBlockingTest {
|
||||
val intent = Intent()
|
||||
|
@ -185,7 +193,6 @@ class IntentReceiverActivityTest {
|
|||
}
|
||||
|
||||
private fun attachMocks(activity: Activity) {
|
||||
mockkStatic("org.mozilla.fenix.ext.ContextKt")
|
||||
every { activity.settings() } returns settings
|
||||
every { activity.components.analytics } returns mockk(relaxed = true)
|
||||
every { activity.components.intentProcessors } returns intentProcessors
|
||||
|
|
|
@ -81,25 +81,28 @@ class AddonDetailsViewTest {
|
|||
|
||||
@Test
|
||||
fun `bind addons version`() {
|
||||
detailsView.bind(baseAddon.copy(
|
||||
val addon1 = baseAddon.copy(
|
||||
version = "1.0.0",
|
||||
installedState = null
|
||||
))
|
||||
)
|
||||
|
||||
detailsView.bind(addon1)
|
||||
assertEquals("1.0.0", view.version_text.text)
|
||||
view.version_text.performLongClick()
|
||||
verify(exactly = 0) { interactor.showUpdaterDialog(any()) }
|
||||
verify(exactly = 0) { interactor.showUpdaterDialog(addon1) }
|
||||
|
||||
detailsView.bind(baseAddon.copy(
|
||||
val addon2 = baseAddon.copy(
|
||||
version = "1.0.0",
|
||||
installedState = Addon.InstalledState(
|
||||
id = "",
|
||||
version = "2.0.0",
|
||||
optionsPageUrl = null
|
||||
)
|
||||
))
|
||||
)
|
||||
detailsView.bind(addon2)
|
||||
assertEquals("2.0.0", view.version_text.text)
|
||||
view.version_text.performLongClick()
|
||||
verify { interactor.showUpdaterDialog(any()) }
|
||||
verify { interactor.showUpdaterDialog(addon2) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -32,12 +32,12 @@ class DefaultBrowsingModeManagerTest {
|
|||
manager.mode = BrowsingMode.Private
|
||||
manager.mode = BrowsingMode.Private
|
||||
|
||||
verify(exactly = 3) { callback.invoke(any()) }
|
||||
verify(exactly = 3) { callback.invoke(BrowsingMode.Private) }
|
||||
|
||||
manager.mode = BrowsingMode.Normal
|
||||
manager.mode = BrowsingMode.Normal
|
||||
|
||||
verify(exactly = 5) { callback.invoke(any()) }
|
||||
verify(exactly = 2) { callback.invoke(BrowsingMode.Normal) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/* 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.collections
|
||||
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.android.synthetic.main.collection_tab_list_row.*
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.home.Tab
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class CollectionCreationTabListAdapterTest {
|
||||
|
||||
private lateinit var interactor: CollectionCreationInteractor
|
||||
private lateinit var adapter: CollectionCreationTabListAdapter
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
interactor = mockk()
|
||||
adapter = CollectionCreationTabListAdapter(interactor)
|
||||
|
||||
every { interactor.selectCollection(any(), any()) } just Runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getItemCount should return the number of tab collections`() {
|
||||
val tab = mockk<Tab>()
|
||||
|
||||
assertEquals(0, adapter.itemCount)
|
||||
|
||||
adapter.updateData(
|
||||
tabs = listOf(tab),
|
||||
selectedTabs = emptySet()
|
||||
)
|
||||
assertEquals(1, adapter.itemCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates and binds viewholder`() {
|
||||
val tab = mockk<Tab> {
|
||||
every { sessionId } returns "abc"
|
||||
every { title } returns "Mozilla"
|
||||
every { hostname } returns "mozilla.org"
|
||||
every { url } returns "https://mozilla.org"
|
||||
}
|
||||
adapter.updateData(
|
||||
tabs = listOf(tab),
|
||||
selectedTabs = emptySet()
|
||||
)
|
||||
|
||||
val holder = adapter.createViewHolder(FrameLayout(testContext), 0)
|
||||
adapter.bindViewHolder(holder, 0)
|
||||
|
||||
assertEquals("Mozilla", holder.tab_title.text)
|
||||
assertEquals("mozilla.org", holder.hostname.text)
|
||||
assertFalse(holder.tab_selected_checkbox.isInvisible)
|
||||
assertTrue(holder.itemView.isClickable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateData inserts item`() {
|
||||
val tab = mockk<Tab> {
|
||||
every { sessionId } returns "abc"
|
||||
}
|
||||
val observer = mockk<RecyclerView.AdapterDataObserver>(relaxed = true)
|
||||
adapter.registerAdapterDataObserver(observer)
|
||||
adapter.updateData(
|
||||
tabs = listOf(tab),
|
||||
selectedTabs = emptySet()
|
||||
)
|
||||
|
||||
verify { observer.onItemRangeInserted(0, 1) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/* 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.collections
|
||||
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.android.synthetic.main.collections_list_item.view.*
|
||||
import mozilla.components.feature.tab.collections.TabCollection
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class SaveCollectionListAdapterTest {
|
||||
|
||||
private lateinit var parent: ViewGroup
|
||||
private lateinit var interactor: CollectionCreationInteractor
|
||||
private lateinit var adapter: SaveCollectionListAdapter
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
parent = FrameLayout(testContext)
|
||||
interactor = mockk()
|
||||
adapter = SaveCollectionListAdapter(interactor)
|
||||
|
||||
every { interactor.selectCollection(any(), any()) } just Runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getItemCount should return the number of tab collections`() {
|
||||
val collection = mockk<TabCollection>()
|
||||
|
||||
assertEquals(0, adapter.itemCount)
|
||||
|
||||
adapter.updateData(
|
||||
tabCollections = listOf(collection),
|
||||
selectedTabs = emptySet()
|
||||
)
|
||||
assertEquals(1, adapter.itemCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates and binds viewholder`() {
|
||||
val collection = mockk<TabCollection> {
|
||||
every { id } returns 0L
|
||||
every { title } returns "Collection"
|
||||
every { tabs } returns listOf(
|
||||
mockk {
|
||||
every { url } returns "https://mozilla.org"
|
||||
},
|
||||
mockk {
|
||||
every { url } returns "https://firefox.com"
|
||||
}
|
||||
)
|
||||
}
|
||||
adapter.updateData(
|
||||
tabCollections = listOf(collection),
|
||||
selectedTabs = emptySet()
|
||||
)
|
||||
|
||||
val holder = adapter.createViewHolder(parent, 0)
|
||||
adapter.bindViewHolder(holder, 0)
|
||||
|
||||
assertEquals("Collection", holder.itemView.collection_item.text)
|
||||
assertEquals("mozilla.org, firefox.com", holder.itemView.collection_description.text)
|
||||
|
||||
holder.itemView.performClick()
|
||||
verify { interactor.selectCollection(collection, emptyList()) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
/* 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.collections
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.home.Tab
|
||||
|
||||
class TabDiffUtilTest {
|
||||
|
||||
@Test
|
||||
fun `list size is returned`() {
|
||||
val diffUtil = TabDiffUtil(
|
||||
old = listOf(mockk(), mockk()),
|
||||
new = listOf(mockk()),
|
||||
oldSelected = emptySet(),
|
||||
newSelected = emptySet(),
|
||||
oldHideCheckboxes = false,
|
||||
newHideCheckboxes = false
|
||||
)
|
||||
|
||||
assertEquals(2, diffUtil.oldListSize)
|
||||
assertEquals(1, diffUtil.newListSize)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `single lists are the same`() {
|
||||
val tab = mockk<Tab> {
|
||||
every { sessionId } returns "abc"
|
||||
}
|
||||
val diffUtil = TabDiffUtil(
|
||||
old = listOf(tab),
|
||||
new = listOf(tab),
|
||||
oldSelected = emptySet(),
|
||||
newSelected = emptySet(),
|
||||
oldHideCheckboxes = false,
|
||||
newHideCheckboxes = false
|
||||
)
|
||||
|
||||
assertTrue(diffUtil.areItemsTheSame(0, 0))
|
||||
assertTrue(diffUtil.areContentsTheSame(0, 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `selection affects contents`() {
|
||||
val tab = mockk<Tab> {
|
||||
every { sessionId } returns "abc"
|
||||
}
|
||||
val diffUtil = TabDiffUtil(
|
||||
old = listOf(tab),
|
||||
new = listOf(tab),
|
||||
oldSelected = emptySet(),
|
||||
newSelected = setOf(tab),
|
||||
oldHideCheckboxes = false,
|
||||
newHideCheckboxes = false
|
||||
)
|
||||
|
||||
assertTrue(diffUtil.areItemsTheSame(0, 0))
|
||||
assertFalse(diffUtil.areContentsTheSame(0, 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hide checkboxes affects contents`() {
|
||||
val tab = mockk<Tab> {
|
||||
every { sessionId } returns "abc"
|
||||
}
|
||||
val diffUtil = TabDiffUtil(
|
||||
old = listOf(tab),
|
||||
new = listOf(tab),
|
||||
oldSelected = setOf(tab),
|
||||
newSelected = setOf(tab),
|
||||
oldHideCheckboxes = false,
|
||||
newHideCheckboxes = true
|
||||
)
|
||||
|
||||
assertTrue(diffUtil.areItemsTheSame(0, 0))
|
||||
assertFalse(diffUtil.areContentsTheSame(0, 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `change payload covers no change case`() {
|
||||
val tab = mockk<Tab>()
|
||||
val payload = TabDiffUtil(
|
||||
old = listOf(tab),
|
||||
new = listOf(tab),
|
||||
oldSelected = setOf(tab),
|
||||
newSelected = setOf(tab),
|
||||
oldHideCheckboxes = false,
|
||||
newHideCheckboxes = false
|
||||
).getChangePayload(0, 0)
|
||||
|
||||
assertEquals(
|
||||
CheckChanged(
|
||||
shouldBeChecked = false,
|
||||
shouldBeUnchecked = false,
|
||||
shouldHideCheckBox = false
|
||||
),
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `include shouldBeChecked in change payload`() {
|
||||
val tab = mockk<Tab>()
|
||||
val payload = TabDiffUtil(
|
||||
old = listOf(tab),
|
||||
new = listOf(tab),
|
||||
oldSelected = emptySet(),
|
||||
newSelected = setOf(tab),
|
||||
oldHideCheckboxes = false,
|
||||
newHideCheckboxes = false
|
||||
).getChangePayload(0, 0)
|
||||
|
||||
assertEquals(
|
||||
CheckChanged(
|
||||
shouldBeChecked = true,
|
||||
shouldBeUnchecked = false,
|
||||
shouldHideCheckBox = false
|
||||
),
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `include shouldBeUnchecked in change payload`() {
|
||||
val tab = mockk<Tab>()
|
||||
val payload = TabDiffUtil(
|
||||
old = listOf(tab),
|
||||
new = listOf(tab),
|
||||
oldSelected = setOf(tab),
|
||||
newSelected = emptySet(),
|
||||
oldHideCheckboxes = false,
|
||||
newHideCheckboxes = true
|
||||
).getChangePayload(0, 0)
|
||||
|
||||
assertEquals(
|
||||
CheckChanged(
|
||||
shouldBeChecked = false,
|
||||
shouldBeUnchecked = true,
|
||||
shouldHideCheckBox = true
|
||||
),
|
||||
payload
|
||||
)
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import mozilla.components.lib.crash.Crash
|
|||
import mozilla.components.lib.crash.CrashReporter
|
||||
import mozilla.components.lib.crash.service.CrashReporterService
|
||||
import mozilla.components.support.base.crash.Breadcrumb
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
internal class BreadcrumbRecorderTest {
|
||||
|
@ -36,17 +37,18 @@ internal class BreadcrumbRecorderTest {
|
|||
)
|
||||
)
|
||||
|
||||
fun getBreadcrumbMessage(@Suppress("UNUSED_PARAMETER") destination: NavDestination): String {
|
||||
return "test"
|
||||
}
|
||||
|
||||
val navController: NavController = mockk()
|
||||
val navDestination: NavDestination = mockk()
|
||||
|
||||
val breadCrumbRecorder =
|
||||
BreadcrumbsRecorder(reporter, navController, ::getBreadcrumbMessage)
|
||||
val breadCrumbRecorder = BreadcrumbsRecorder(reporter, navController) { "test" }
|
||||
breadCrumbRecorder.onDestinationChanged(navController, navDestination, null)
|
||||
|
||||
verify { reporter.recordCrashBreadcrumb(any()) }
|
||||
verify {
|
||||
reporter.recordCrashBreadcrumb(withArg {
|
||||
assertEquals("test", it.message)
|
||||
assertEquals("DestinationChanged", it.category)
|
||||
assertEquals(Breadcrumb.Level.INFO, it.level)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.Intent.ACTION_VIEW
|
||||
import androidx.core.net.toUri
|
||||
import io.mockk.MockKMatcherScope
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
|
@ -32,6 +31,7 @@ import org.mozilla.fenix.R
|
|||
import org.mozilla.fenix.components.metrics.MozillaProductDetector
|
||||
import org.mozilla.fenix.components.metrics.MozillaProductDetector.MozillaProducts
|
||||
import org.mozilla.fenix.components.tips.TipType
|
||||
import org.mozilla.fenix.ext.intentFilterEq
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.settings.SupportUtils
|
||||
|
@ -205,7 +205,4 @@ class MigrationTipProviderTest {
|
|||
every { settings.shouldDisplayFenixMovingTip() } returns true
|
||||
assertTrue(MigrationTipProvider(context).shouldDisplay)
|
||||
}
|
||||
|
||||
private fun MockKMatcherScope.intentFilterEq(value: Intent): Intent =
|
||||
match { it.filterEquals(value) }
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.mozilla.fenix.ext
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.navigation.NavDirections
|
||||
import io.mockk.Matcher
|
||||
import io.mockk.MockKMatcherScope
|
||||
|
@ -11,6 +12,13 @@ import mozilla.components.support.ktx.android.os.contentEquals
|
|||
*/
|
||||
fun MockKMatcherScope.directionsEq(value: NavDirections) = match(EqNavDirectionsMatcher(value))
|
||||
|
||||
/**
|
||||
* Verify that two intents are the same for the purposes of intent resolution (filtering).
|
||||
* Checks if their action, data, type, identity, class, and categories are the same.
|
||||
* Does not compare extras.
|
||||
*/
|
||||
fun MockKMatcherScope.intentFilterEq(value: Intent) = match(EqIntentFilterMatcher(value))
|
||||
|
||||
private data class EqNavDirectionsMatcher(private val value: NavDirections) : Matcher<NavDirections> {
|
||||
|
||||
override fun match(arg: NavDirections?): Boolean =
|
||||
|
@ -19,3 +27,13 @@ private data class EqNavDirectionsMatcher(private val value: NavDirections) : Ma
|
|||
override fun substitute(map: Map<Any, Any>) =
|
||||
copy(value = value.internalSubstitute(map))
|
||||
}
|
||||
|
||||
private data class EqIntentFilterMatcher(private val value: Intent) : Matcher<Intent> {
|
||||
|
||||
override fun match(arg: Intent?): Boolean = value.filterEquals(arg)
|
||||
|
||||
override fun substitute(map: Map<Any, Any>) =
|
||||
copy(value = value.internalSubstitute(map))
|
||||
|
||||
override fun toString() = "intentFilterEq($value)"
|
||||
}
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
package org.mozilla.fenix.home
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDirections
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
import mozilla.components.browser.session.SessionManager
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.feature.tab.collections.TabCollection
|
||||
|
@ -23,12 +23,11 @@ import org.junit.Test
|
|||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.collections.SaveCollectionStep
|
||||
import org.mozilla.fenix.components.TabCollectionStorage
|
||||
import org.mozilla.fenix.components.TopSiteStorage
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.nav
|
||||
import org.mozilla.fenix.components.tips.Tip
|
||||
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
|
||||
import org.mozilla.fenix.settings.SupportUtils
|
||||
import mozilla.components.feature.tab.collections.Tab as ComponentTab
|
||||
|
@ -42,77 +41,138 @@ class DefaultSessionControlControllerTest {
|
|||
private val activity: HomeActivity = mockk(relaxed = true)
|
||||
private val fragmentStore: HomeFragmentStore = mockk(relaxed = true)
|
||||
private val navController: NavController = mockk(relaxed = true)
|
||||
private val getListOfTabs: () -> List<Tab> = { emptyList() }
|
||||
private val metrics: MetricController = mockk(relaxed = true)
|
||||
private val sessionManager: SessionManager = mockk(relaxed = true)
|
||||
private val engine: Engine = mockk(relaxed = true)
|
||||
private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true)
|
||||
private val topSiteStorage: TopSiteStorage = mockk(relaxed = true)
|
||||
private val tabsUseCases: TabsUseCases = mockk(relaxed = true)
|
||||
|
||||
private val hideOnboarding: () -> Unit = mockk(relaxed = true)
|
||||
private val openSettingsScreen: () -> Unit = mockk(relaxed = true)
|
||||
private val openWhatsNewLink: () -> Unit = mockk(relaxed = true)
|
||||
private val openPrivacyNotice: () -> Unit = mockk(relaxed = true)
|
||||
private val registerCollectionStorageObserver: () -> Unit = mockk(relaxed = true)
|
||||
private val showTabTray: () -> Unit = mockk(relaxed = true)
|
||||
private val showDeleteCollectionPrompt: (tabCollection: TabCollection, title: String?, message: String) -> Unit =
|
||||
mockk(relaxed = true)
|
||||
private val metrics: MetricController = mockk(relaxed = true)
|
||||
private val state: HomeFragmentState = mockk(relaxed = true)
|
||||
private val sessionManager: SessionManager = mockk(relaxed = true)
|
||||
private val engine: Engine = mockk(relaxed = true)
|
||||
private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true)
|
||||
private val tabsUseCases: TabsUseCases = mockk(relaxed = true)
|
||||
|
||||
private lateinit var controller: DefaultSessionControlController
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockkStatic("org.mozilla.fenix.ext.ContextKt")
|
||||
every { activity.components.core.engine } returns engine
|
||||
every { activity.components.core.sessionManager } returns sessionManager
|
||||
every { activity.components.core.tabCollectionStorage } returns tabCollectionStorage
|
||||
every { activity.components.useCases.tabsUseCases } returns tabsUseCases
|
||||
|
||||
every { fragmentStore.state } returns state
|
||||
every { state.collections } returns emptyList()
|
||||
every { state.expandedCollections } returns emptySet()
|
||||
every { state.mode } returns Mode.Normal
|
||||
every { activity.components.analytics.metrics } returns metrics
|
||||
every { fragmentStore.state } returns HomeFragmentState(
|
||||
collections = emptyList(),
|
||||
expandedCollections = emptySet(),
|
||||
mode = Mode.Normal,
|
||||
topSites = emptyList()
|
||||
)
|
||||
every { sessionManager.sessions } returns emptyList()
|
||||
every { navController.currentDestination } returns mockk {
|
||||
every { id } returns R.id.homeFragment
|
||||
}
|
||||
|
||||
controller = DefaultSessionControlController(
|
||||
activity = activity,
|
||||
engine = engine,
|
||||
metrics = metrics,
|
||||
sessionManager = sessionManager,
|
||||
tabCollectionStorage = tabCollectionStorage,
|
||||
topSiteStorage = topSiteStorage,
|
||||
addTabUseCase = tabsUseCases.addTab,
|
||||
fragmentStore = fragmentStore,
|
||||
navController = navController,
|
||||
viewLifecycleScope = MainScope(),
|
||||
getListOfTabs = getListOfTabs,
|
||||
viewLifecycleScope = TestCoroutineScope(),
|
||||
hideOnboarding = hideOnboarding,
|
||||
registerCollectionStorageObserver = registerCollectionStorageObserver,
|
||||
showDeleteCollectionPrompt = showDeleteCollectionPrompt,
|
||||
openSettingsScreen = openSettingsScreen,
|
||||
openWhatsNewLink = openWhatsNewLink,
|
||||
openPrivacyNotice = openPrivacyNotice,
|
||||
showTabTray = showTabTray
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleCollectionAddTabTapped() {
|
||||
val collection: TabCollection = mockk(relaxed = true)
|
||||
val collection = mockk<TabCollection> {
|
||||
every { id } returns 12L
|
||||
}
|
||||
controller.handleCollectionAddTabTapped(collection)
|
||||
|
||||
verify { metrics.track(Event.CollectionAddTabPressed) }
|
||||
verify {
|
||||
navController.navigate(
|
||||
match<NavDirections> { it.actionId == R.id.action_global_collectionCreationFragment },
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleCollectionOpenTabClicked() {
|
||||
val tab: ComponentTab = mockk(relaxed = true)
|
||||
fun `handleCollectionOpenTabClicked onFailure`() {
|
||||
val tab = mockk<ComponentTab> {
|
||||
every { url } returns "https://mozilla.org"
|
||||
every { restore(activity, engine, restoreSessionId = false) } returns null
|
||||
}
|
||||
controller.handleCollectionOpenTabClicked(tab)
|
||||
|
||||
verify { metrics.track(Event.CollectionTabRestored) }
|
||||
verify {
|
||||
activity.openToBrowserAndLoad(
|
||||
searchTermOrURL = "https://mozilla.org",
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromHome
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleCollectionOpenTabClicked onTabRestored`() {
|
||||
val tab = mockk<ComponentTab> {
|
||||
every { restore(activity, engine, restoreSessionId = false) } returns mockk {
|
||||
every { session } returns mockk()
|
||||
every { engineSessionState } returns mockk()
|
||||
}
|
||||
}
|
||||
controller.handleCollectionOpenTabClicked(tab)
|
||||
|
||||
verify { metrics.track(Event.CollectionTabRestored) }
|
||||
verify { activity.openToBrowser(BrowserDirection.FromHome) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleCollectionOpenTabsTapped() {
|
||||
val collection: TabCollection = mockk(relaxed = true)
|
||||
val collection = mockk<TabCollection> {
|
||||
every { tabs } returns emptyList()
|
||||
}
|
||||
controller.handleCollectionOpenTabsTapped(collection)
|
||||
|
||||
verify { metrics.track(Event.CollectionAllTabsRestored) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleCollectionRemoveTab() {
|
||||
fun `handleCollectionRemoveTab one tab`() {
|
||||
val collection = mockk<TabCollection> {
|
||||
every { tabs } returns listOf(mockk())
|
||||
every { title } returns "Collection"
|
||||
}
|
||||
val tab = mockk<ComponentTab>()
|
||||
every {
|
||||
activity.resources.getString(R.string.delete_tab_and_collection_dialog_title, "Collection")
|
||||
} returns "Delete Collection?"
|
||||
every {
|
||||
activity.resources.getString(R.string.delete_tab_and_collection_dialog_message)
|
||||
} returns "Deleting this tab will delete everything."
|
||||
|
||||
controller.handleCollectionRemoveTab(collection, tab)
|
||||
|
||||
verify { metrics.track(Event.CollectionTabRemoved) }
|
||||
verify {
|
||||
showDeleteCollectionPrompt(
|
||||
collection,
|
||||
"Delete Collection?",
|
||||
"Deleting this tab will delete everything."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleCollectionRemoveTab multiple tabs`() {
|
||||
val collection: TabCollection = mockk(relaxed = true)
|
||||
val tab: ComponentTab = mockk(relaxed = true)
|
||||
controller.handleCollectionRemoveTab(collection, tab)
|
||||
|
@ -121,16 +181,37 @@ class DefaultSessionControlControllerTest {
|
|||
|
||||
@Test
|
||||
fun handleCollectionShareTabsClicked() {
|
||||
val collection: TabCollection = mockk(relaxed = true)
|
||||
val collection = mockk<TabCollection> {
|
||||
every { tabs } returns emptyList()
|
||||
}
|
||||
controller.handleCollectionShareTabsClicked(collection)
|
||||
|
||||
verify { metrics.track(Event.CollectionShared) }
|
||||
verify {
|
||||
navController.navigate(
|
||||
match<NavDirections> { it.actionId == R.id.action_global_shareFragment },
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleDeleteCollectionTapped() {
|
||||
val collection: TabCollection = mockk(relaxed = true)
|
||||
val collection = mockk<TabCollection> {
|
||||
every { title } returns "Collection"
|
||||
}
|
||||
every {
|
||||
activity.resources.getString(R.string.tab_collection_dialog_message, "Collection")
|
||||
} returns "Are you sure you want to delete Collection?"
|
||||
|
||||
controller.handleDeleteCollectionTapped(collection)
|
||||
verify { showDeleteCollectionPrompt(collection, null, any()) }
|
||||
verify {
|
||||
showDeleteCollectionPrompt(
|
||||
collection,
|
||||
null,
|
||||
"Are you sure you want to delete Collection?"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -148,9 +229,18 @@ class DefaultSessionControlControllerTest {
|
|||
|
||||
@Test
|
||||
fun handleRenameCollectionTapped() {
|
||||
val collection: TabCollection = mockk(relaxed = true)
|
||||
val collection = mockk<TabCollection> {
|
||||
every { id } returns 3L
|
||||
}
|
||||
controller.handleRenameCollectionTapped(collection)
|
||||
|
||||
verify { metrics.track(Event.CollectionRenamePressed) }
|
||||
verify {
|
||||
navController.navigate(
|
||||
match<NavDirections> { it.actionId == R.id.action_global_collectionCreationFragment },
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -191,20 +281,62 @@ class DefaultSessionControlControllerTest {
|
|||
@Test
|
||||
fun handleOpenSettingsClicked() {
|
||||
controller.handleOpenSettingsClicked()
|
||||
verify { openSettingsScreen() }
|
||||
verify {
|
||||
navController.navigate(
|
||||
match<NavDirections> { it.actionId == R.id.action_global_privateBrowsingFragment },
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleWhatsNewGetAnswersClicked() {
|
||||
controller.handleWhatsNewGetAnswersClicked()
|
||||
val whatsNewUrl = SupportUtils.getWhatsNewUrl(activity)
|
||||
verify {
|
||||
activity.openToBrowserAndLoad(
|
||||
searchTermOrURL = whatsNewUrl,
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromHome
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleReadPrivacyNoticeClicked() {
|
||||
controller.handleReadPrivacyNoticeClicked()
|
||||
verify {
|
||||
activity.openToBrowserAndLoad(
|
||||
searchTermOrURL = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromHome
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleToggleCollectionExpanded() {
|
||||
val collection: TabCollection = mockk(relaxed = true)
|
||||
val collection = mockk<TabCollection>()
|
||||
controller.handleToggleCollectionExpanded(collection, true)
|
||||
verify { fragmentStore.dispatch(HomeFragmentAction.CollectionExpanded(collection, true)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleCloseTip() {
|
||||
val tip = mockk<Tip>()
|
||||
controller.handleCloseTip(tip)
|
||||
verify { fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleCreateCollection() {
|
||||
controller.handleCreateCollection()
|
||||
val directions = HomeFragmentDirections.actionGlobalCollectionCreationFragment(saveCollectionStep = SaveCollectionStep.SelectTabs)
|
||||
verify { navController.nav(R.id.homeFragment, directions) }
|
||||
|
||||
verify {
|
||||
navController.navigate(
|
||||
match<NavDirections> { it.actionId == R.id.action_global_collectionCreationFragment },
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ class BookmarkControllerTest {
|
|||
private val loadBookmarkNode: suspend (String) -> BookmarkNode? = mockk(relaxed = true)
|
||||
private val showSnackbar: (String) -> Unit = mockk(relaxed = true)
|
||||
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit = mockk(relaxed = true)
|
||||
private val deleteBookmarkFolder: (BookmarkNode) -> Unit = mockk(relaxed = true)
|
||||
private val deleteBookmarkFolder: (Set<BookmarkNode>) -> Unit = mockk(relaxed = true)
|
||||
private val invokePendingDeletion: () -> Unit = mockk(relaxed = true)
|
||||
|
||||
private val homeActivity: HomeActivity = mockk(relaxed = true)
|
||||
|
@ -304,10 +304,10 @@ class BookmarkControllerTest {
|
|||
|
||||
@Test
|
||||
fun `handleBookmarkDeletion for a folder should properly call the delete folder delegate`() {
|
||||
controller.handleBookmarkFolderDeletion(subfolder)
|
||||
controller.handleBookmarkFolderDeletion(setOf(subfolder))
|
||||
|
||||
verify {
|
||||
deleteBookmarkFolder(subfolder)
|
||||
deleteBookmarkFolder(setOf(subfolder))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -180,7 +180,7 @@ class BookmarkFragmentInteractorTest {
|
|||
interactor.onDelete(setOf(subfolder))
|
||||
|
||||
verify {
|
||||
bookmarkController.handleBookmarkFolderDeletion(subfolder)
|
||||
bookmarkController.handleBookmarkFolderDeletion(setOf(subfolder))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,9 @@ class HistoryFragmentStoreTest {
|
|||
fun finishSync() = runBlocking {
|
||||
val initialState = HistoryFragmentState(
|
||||
items = listOf(),
|
||||
mode = HistoryFragmentState.Mode.Syncing
|
||||
mode = HistoryFragmentState.Mode.Syncing,
|
||||
pendingDeletionIds = emptySet(),
|
||||
isDeletingItems = false
|
||||
)
|
||||
val store = HistoryFragmentStore(initialState)
|
||||
|
||||
|
@ -71,16 +73,22 @@ class HistoryFragmentStoreTest {
|
|||
|
||||
private fun emptyDefaultState(): HistoryFragmentState = HistoryFragmentState(
|
||||
items = listOf(),
|
||||
mode = HistoryFragmentState.Mode.Normal
|
||||
mode = HistoryFragmentState.Mode.Normal,
|
||||
pendingDeletionIds = emptySet(),
|
||||
isDeletingItems = false
|
||||
)
|
||||
|
||||
private fun oneItemEditState(): HistoryFragmentState = HistoryFragmentState(
|
||||
items = listOf(),
|
||||
mode = HistoryFragmentState.Mode.Editing(setOf(historyItem))
|
||||
mode = HistoryFragmentState.Mode.Editing(setOf(historyItem)),
|
||||
pendingDeletionIds = emptySet(),
|
||||
isDeletingItems = false
|
||||
)
|
||||
|
||||
private fun twoItemEditState(): HistoryFragmentState = HistoryFragmentState(
|
||||
items = listOf(),
|
||||
mode = HistoryFragmentState.Mode.Editing(setOf(historyItem, newHistoryItem))
|
||||
mode = HistoryFragmentState.Mode.Editing(setOf(historyItem, newHistoryItem)),
|
||||
pendingDeletionIds = emptySet(),
|
||||
isDeletingItems = false
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/* 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.widget.LinearLayout
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsDeleteButtonViewHolder
|
||||
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsHeaderViewHolder
|
||||
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsListItemViewHolder
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class LoginExceptionsAdapterTest {
|
||||
|
||||
private lateinit var interactor: LoginExceptionsInteractor
|
||||
private lateinit var adapter: LoginExceptionsAdapter
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
interactor = mockk()
|
||||
adapter = LoginExceptionsAdapter(interactor)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates correct view holder type`() {
|
||||
val parent = LinearLayout(ContextThemeWrapper(testContext, R.style.NormalTheme))
|
||||
adapter.updateData(listOf(mockk(), mockk()))
|
||||
assertEquals(4, adapter.itemCount)
|
||||
|
||||
val holders = (0 until adapter.itemCount).asSequence()
|
||||
.map { i -> adapter.getItemViewType(i) }
|
||||
.map { viewType -> adapter.onCreateViewHolder(parent, viewType) }
|
||||
.toList()
|
||||
assertEquals(4, holders.size)
|
||||
|
||||
assertTrue(holders[0] is LoginExceptionsHeaderViewHolder)
|
||||
assertTrue(holders[1] is LoginExceptionsListItemViewHolder)
|
||||
assertTrue(holders[2] is LoginExceptionsListItemViewHolder)
|
||||
assertTrue(holders[3] is LoginExceptionsDeleteButtonViewHolder)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `headers and delete should check if the other object is the same`() {
|
||||
assertTrue(LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
|
||||
LoginExceptionsAdapter.AdapterItem.Header,
|
||||
LoginExceptionsAdapter.AdapterItem.Header
|
||||
))
|
||||
assertTrue(LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
|
||||
LoginExceptionsAdapter.AdapterItem.DeleteButton,
|
||||
LoginExceptionsAdapter.AdapterItem.DeleteButton
|
||||
))
|
||||
assertFalse(LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
|
||||
LoginExceptionsAdapter.AdapterItem.Header,
|
||||
LoginExceptionsAdapter.AdapterItem.DeleteButton
|
||||
))
|
||||
assertTrue(LoginExceptionsAdapter.DiffCallback.areContentsTheSame(
|
||||
LoginExceptionsAdapter.AdapterItem.Header,
|
||||
LoginExceptionsAdapter.AdapterItem.Header
|
||||
))
|
||||
assertTrue(LoginExceptionsAdapter.DiffCallback.areContentsTheSame(
|
||||
LoginExceptionsAdapter.AdapterItem.DeleteButton,
|
||||
LoginExceptionsAdapter.AdapterItem.DeleteButton
|
||||
))
|
||||
assertFalse(LoginExceptionsAdapter.DiffCallback.areContentsTheSame(
|
||||
LoginExceptionsAdapter.AdapterItem.DeleteButton,
|
||||
LoginExceptionsAdapter.AdapterItem.Header
|
||||
))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `items with the same id should be marked as same`() {
|
||||
assertTrue(LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
|
||||
LoginExceptionsAdapter.AdapterItem.Item(mockk {
|
||||
every { id } returns 12L
|
||||
}),
|
||||
LoginExceptionsAdapter.AdapterItem.Item(mockk {
|
||||
every { id } returns 12L
|
||||
})
|
||||
))
|
||||
assertFalse(LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
|
||||
LoginExceptionsAdapter.AdapterItem.Item(mockk {
|
||||
every { id } returns 14L
|
||||
}),
|
||||
LoginExceptionsAdapter.AdapterItem.Item(mockk {
|
||||
every { id } returns 12L
|
||||
})
|
||||
))
|
||||
assertFalse(LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
|
||||
LoginExceptionsAdapter.AdapterItem.Item(mockk {
|
||||
every { id } returns 14L
|
||||
}),
|
||||
LoginExceptionsAdapter.AdapterItem.Header
|
||||
))
|
||||
assertFalse(LoginExceptionsAdapter.DiffCallback.areItemsTheSame(
|
||||
LoginExceptionsAdapter.AdapterItem.DeleteButton,
|
||||
LoginExceptionsAdapter.AdapterItem.Item(mockk {
|
||||
every { id } returns 14L
|
||||
})
|
||||
))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/* 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.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import io.mockk.mockk
|
||||
import kotlinx.android.synthetic.main.component_exceptions.*
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class LoginExceptionsViewTest {
|
||||
|
||||
private lateinit var parent: ViewGroup
|
||||
private lateinit var interactor: LoginExceptionsInteractor
|
||||
private lateinit var view: LoginExceptionsView
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
parent = FrameLayout(testContext)
|
||||
interactor = mockk()
|
||||
view = LoginExceptionsView(parent, interactor)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sets empty message text`() {
|
||||
assertEquals(
|
||||
"Logins and passwords that are not saved will be shown here.",
|
||||
view.exceptions_empty_message.text
|
||||
)
|
||||
assertTrue(view.exceptions_list.adapter is LoginExceptionsAdapter)
|
||||
assertTrue(view.exceptions_list.layoutManager is LinearLayoutManager)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hide list when there are no items`() {
|
||||
view.update(ExceptionsFragmentState(
|
||||
items = emptyList()
|
||||
))
|
||||
|
||||
assertTrue(view.exceptions_empty_view.isVisible)
|
||||
assertFalse(view.exceptions_list.isVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shows list when there are items`() {
|
||||
view.update(ExceptionsFragmentState(
|
||||
items = listOf(mockk())
|
||||
))
|
||||
|
||||
assertFalse(view.exceptions_empty_view.isVisible)
|
||||
assertTrue(view.exceptions_list.isVisible)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/* 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 com.google.android.material.button.MaterialButton
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.loginexceptions.LoginExceptionsInteractor
|
||||
|
||||
class LoginExceptionsDeleteButtonViewHolderTest {
|
||||
|
||||
private lateinit var view: View
|
||||
private lateinit var deleteButton: MaterialButton
|
||||
private lateinit var interactor: LoginExceptionsInteractor
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
deleteButton = mockk()
|
||||
view = mockk {
|
||||
every { findViewById<MaterialButton>(R.id.removeAllExceptions) } returns deleteButton
|
||||
}
|
||||
interactor = mockk()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete button calls interactor`() {
|
||||
val slot = slot<View.OnClickListener>()
|
||||
every { deleteButton.setOnClickListener(capture(slot)) } just Runs
|
||||
LoginExceptionsDeleteButtonViewHolder(view, interactor)
|
||||
|
||||
every { interactor.onDeleteAll() } just Runs
|
||||
slot.captured.onClick(mockk())
|
||||
verify { interactor.onDeleteAll() }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/* 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 android.widget.TextView
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
class LoginExceptionsHeaderViewHolderTest {
|
||||
|
||||
private lateinit var view: View
|
||||
private lateinit var description: TextView
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
description = mockk(relaxUnitFun = true)
|
||||
view = mockk {
|
||||
every { findViewById<TextView>(R.id.exceptions_description) } returns description
|
||||
every {
|
||||
context.getString(R.string.preferences_passwords_exceptions_description)
|
||||
} returns "Logins and passwords will not be saved for these sites."
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sets description text`() {
|
||||
LoginExceptionsHeaderViewHolder(view)
|
||||
verify { description.text = "Logins and passwords will not be saved for these sites." }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/* 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 android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import mozilla.components.feature.logins.exceptions.LoginException
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.loginexceptions.LoginExceptionsInteractor
|
||||
|
||||
class LoginExceptionsListItemViewHolderTest {
|
||||
|
||||
private lateinit var view: View
|
||||
private lateinit var url: TextView
|
||||
private lateinit var deleteButton: ImageButton
|
||||
private lateinit var interactor: LoginExceptionsInteractor
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
url = mockk(relaxUnitFun = true)
|
||||
deleteButton = mockk(relaxUnitFun = true)
|
||||
view = mockk {
|
||||
every { findViewById<TextView>(R.id.webAddressView) } returns url
|
||||
every { findViewById<ImageButton>(R.id.delete_exception) } returns deleteButton
|
||||
every { findViewById<ImageView>(R.id.favicon_image) } returns mockk()
|
||||
}
|
||||
interactor = mockk()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sets url text`() {
|
||||
LoginExceptionsListItemViewHolder(view, interactor).bind(mockk {
|
||||
every { origin } returns "mozilla.org"
|
||||
})
|
||||
verify { url.text = "mozilla.org" }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete button calls interactor`() {
|
||||
val slot = slot<View.OnClickListener>()
|
||||
val loginException = mockk<LoginException> {
|
||||
every { origin } returns "mozilla.org"
|
||||
}
|
||||
every { deleteButton.setOnClickListener(capture(slot)) } just Runs
|
||||
LoginExceptionsListItemViewHolder(view, interactor).bind(loginException)
|
||||
|
||||
every { interactor.onDeleteOne(loginException) } just Runs
|
||||
slot.captured.onClick(mockk())
|
||||
verify { interactor.onDeleteOne(loginException) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/* 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.migration
|
||||
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import kotlinx.android.synthetic.main.migration_list_item.view.*
|
||||
import mozilla.components.support.migration.Migration
|
||||
import mozilla.components.support.migration.MigrationRun
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class MigrationStatusAdapterTest {
|
||||
|
||||
private lateinit var adapter: MigrationStatusAdapter
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
adapter = MigrationStatusAdapter()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getItemCount should return the number of items in whitelist`() {
|
||||
assertEquals(0, adapter.itemCount)
|
||||
|
||||
adapter.updateData(mapOf(
|
||||
Migration.Addons to MigrationRun(0, success = true),
|
||||
Migration.Settings to MigrationRun(0, success = true),
|
||||
Migration.Bookmarks to MigrationRun(0, success = false)
|
||||
))
|
||||
assertEquals(4, adapter.itemCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates and binds viewholder`() {
|
||||
adapter.updateData(mapOf(
|
||||
Migration.History to MigrationRun(0, success = true)
|
||||
))
|
||||
|
||||
val holder1 = adapter.createViewHolder(FrameLayout(testContext), 0)
|
||||
val holder2 = adapter.createViewHolder(FrameLayout(testContext), 0)
|
||||
adapter.bindViewHolder(holder1, 0)
|
||||
adapter.bindViewHolder(holder2, 1)
|
||||
|
||||
assertEquals("Settings", holder1.itemView.migration_item_name.text)
|
||||
assertEquals(View.INVISIBLE, holder1.itemView.migration_status_image.visibility)
|
||||
|
||||
assertEquals("History", holder2.itemView.migration_item_name.text)
|
||||
assertEquals(View.VISIBLE, holder2.itemView.migration_status_image.visibility)
|
||||
assertEquals("Migration completed", holder2.itemView.migration_status_image.contentDescription)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/* 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.migration
|
||||
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.verify
|
||||
import io.mockk.verifyOrder
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import mozilla.components.support.migration.state.MigrationAction
|
||||
import mozilla.components.support.migration.state.MigrationStore
|
||||
import mozilla.components.support.test.ext.joinBlocking
|
||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class MigrationTelemetryListenerTest {
|
||||
|
||||
private val testDispatcher = TestCoroutineDispatcher()
|
||||
|
||||
@get:Rule
|
||||
val coroutinesTestRule = MainCoroutineRule(testDispatcher)
|
||||
|
||||
@MockK(relaxed = true) private lateinit var metrics: MetricController
|
||||
@MockK(relaxed = true) private lateinit var logger: Logger
|
||||
private lateinit var store: MigrationStore
|
||||
private lateinit var listener: MigrationTelemetryListener
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
store = MigrationStore()
|
||||
listener = MigrationTelemetryListener(
|
||||
metrics = metrics,
|
||||
store = store,
|
||||
logger = logger
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `progress state is logged`() = testDispatcher.runBlockingTest {
|
||||
listener.start()
|
||||
store.dispatch(MigrationAction.Started).joinBlocking()
|
||||
store.dispatch(MigrationAction.Completed).joinBlocking()
|
||||
store.dispatch(MigrationAction.Clear).joinBlocking()
|
||||
|
||||
verifyOrder {
|
||||
logger.debug("Migration state: MIGRATING")
|
||||
logger.debug("Migration state: COMPLETED")
|
||||
logger.debug("Migration state: NONE")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `metrics are logged when migration is completed`() = testDispatcher.runBlockingTest {
|
||||
listener.start()
|
||||
store.dispatch(MigrationAction.Completed).joinBlocking()
|
||||
|
||||
verify { metrics.track(Event.FennecToFenixMigrated) }
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
package org.mozilla.fenix.search
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDirections
|
||||
import io.mockk.every
|
||||
|
@ -24,7 +25,9 @@ import org.mozilla.fenix.HomeActivity
|
|||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.crashes.CrashListActivity
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.intentFilterEq
|
||||
import org.mozilla.fenix.ext.metrics
|
||||
import org.mozilla.fenix.ext.navigateSafe
|
||||
import org.mozilla.fenix.ext.searchEngineManager
|
||||
|
@ -88,10 +91,13 @@ class DefaultSearchControllerTest {
|
|||
@Test
|
||||
fun handleCrashesUrlCommitted() {
|
||||
val url = "about:crashes"
|
||||
every { activity.packageName } returns testContext.packageName
|
||||
|
||||
controller.handleUrlCommitted(url)
|
||||
|
||||
verify { activity.startActivity(any()) }
|
||||
verify {
|
||||
activity.startActivity(intentFilterEq(Intent(testContext, CrashListActivity::class.java)))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -61,7 +61,7 @@ class InContentTelemetryTest {
|
|||
|
||||
telemetry.processMessage(message)
|
||||
|
||||
verify { telemetry.trackPartnerUrlTypeMetric(url, any()) }
|
||||
verify { telemetry.trackPartnerUrlTypeMetric(url, listOf(first, second)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -44,7 +44,7 @@ class AboutItemViewHolderTest {
|
|||
fun `call listener on click`() {
|
||||
val holder = AboutItemViewHolder(view, listener)
|
||||
holder.bind(item)
|
||||
view.performClick()
|
||||
holder.itemView.performClick()
|
||||
|
||||
verify { listener.onAboutItemClicked(AboutItem.Libraries) }
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ class DefaultDeleteBrowsingDataControllerTest {
|
|||
controller.deleteBrowsingData()
|
||||
|
||||
verify {
|
||||
context.components.core.engine.clearData(any())
|
||||
context.components.core.engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES))
|
||||
context.components.core.historyStorage
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/* 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.settings.logins
|
||||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
||||
import org.mozilla.fenix.settings.logins.interactor.EditLoginInteractor
|
||||
|
||||
class EditLoginInteractorTest {
|
||||
private val loginsController: SavedLoginsStorageController = mockk(relaxed = true)
|
||||
private val interactor = EditLoginInteractor(loginsController)
|
||||
|
||||
@Test
|
||||
fun findPotentialDupesTest() {
|
||||
val id = "anyId"
|
||||
interactor.findPotentialDuplicates(id)
|
||||
verify { loginsController.findPotentialDuplicates(id) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun saveLoginTest() {
|
||||
val id = "anyId"
|
||||
val username = "usernameText"
|
||||
val password = "passwordText"
|
||||
|
||||
interactor.onSaveLogin(id, username, password)
|
||||
|
||||
verify { loginsController.save(id, username, password) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/* 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.settings.logins
|
||||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verifyAll
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
||||
import org.mozilla.fenix.settings.logins.interactor.LoginDetailInteractor
|
||||
|
||||
class LoginDetailInteractorTest {
|
||||
private val loginsController: SavedLoginsStorageController = mockk(relaxed = true)
|
||||
private val interactor = LoginDetailInteractor(loginsController)
|
||||
|
||||
@Test
|
||||
fun fetchLoginListTest() {
|
||||
val id = "anyId"
|
||||
interactor.onFetchLoginList(id)
|
||||
verifyAll { loginsController.fetchLoginDetails(id) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteLoginTest() {
|
||||
val id = "anyId"
|
||||
interactor.onDeleteLogin(id)
|
||||
verifyAll { loginsController.delete(id) }
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import org.junit.Test
|
|||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.settings.logins.view.LoginDetailView
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class LoginDetailViewTest {
|
||||
|
@ -31,7 +32,8 @@ class LoginDetailViewTest {
|
|||
),
|
||||
searchedForText = null,
|
||||
sortingStrategy = SortingStrategy.LastUsed(mockk()),
|
||||
highlightedItem = SavedLoginsSortingStrategyMenu.Item.LastUsedSort
|
||||
highlightedItem = SavedLoginsSortingStrategyMenu.Item.LastUsedSort,
|
||||
duplicateLogins = listOf()
|
||||
)
|
||||
|
||||
private lateinit var view: ViewGroup
|
||||
|
|
|
@ -30,7 +30,8 @@ class LoginsFragmentStoreTest {
|
|||
filteredItems = emptyList(),
|
||||
searchedForText = null,
|
||||
sortingStrategy = SortingStrategy.LastUsed(mockk()),
|
||||
highlightedItem = SavedLoginsSortingStrategyMenu.Item.LastUsedSort
|
||||
highlightedItem = SavedLoginsSortingStrategyMenu.Item.LastUsedSort,
|
||||
duplicateLogins = listOf()
|
||||
)
|
||||
|
||||
@Test
|
||||
|
|
|
@ -6,7 +6,6 @@ package org.mozilla.fenix.settings.logins
|
|||
|
||||
import androidx.navigation.NavController
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import io.mockk.verifyAll
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Test
|
||||
|
@ -16,24 +15,32 @@ import org.mozilla.fenix.components.metrics.Event
|
|||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.settings.SupportUtils
|
||||
import org.mozilla.fenix.settings.logins.controller.LoginsListController
|
||||
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsFragmentDirections
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class SavedLoginsControllerTest {
|
||||
class LoginsListControllerTest {
|
||||
private val store: LoginsFragmentStore = mockk(relaxed = true)
|
||||
private val settings: Settings = mockk(relaxed = true)
|
||||
private val sortingStrategy: SortingStrategy = SortingStrategy.Alphabetically(testContext)
|
||||
private val navController: NavController = mockk(relaxed = true)
|
||||
private val browserNavigator: (String, Boolean, BrowserDirection) -> Unit = mockk(relaxed = true)
|
||||
|
||||
private val settings: Settings = mockk(relaxed = true)
|
||||
private val metrics: MetricController = mockk(relaxed = true)
|
||||
private val sortingStrategy: SortingStrategy = SortingStrategy.Alphabetically(testContext)
|
||||
private val controller = SavedLoginsController(store, navController, browserNavigator, settings, metrics)
|
||||
private val controller =
|
||||
LoginsListController(
|
||||
loginsFragmentStore = store,
|
||||
navController = navController,
|
||||
browserNavigator = browserNavigator,
|
||||
settings = settings,
|
||||
metrics = metrics
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN a sorting strategy, WHEN handleSort is called on the controller, THEN the correct action should be dispatched and the strategy saved in sharedPref`() {
|
||||
controller.handleSort(sortingStrategy)
|
||||
|
||||
verify {
|
||||
verifyAll {
|
||||
store.dispatch(
|
||||
LoginsAction.SortLogins(
|
||||
SortingStrategy.Alphabetically(
|
||||
|
@ -55,7 +62,7 @@ class SavedLoginsControllerTest {
|
|||
store.dispatch(LoginsAction.LoginSelected(login))
|
||||
metrics.track(Event.OpenOneLogin)
|
||||
navController.navigate(
|
||||
SavedLoginsFragmentDirections.actionSavedLoginsFragmentToLoginDetailFragment(login.guid)
|
||||
SavedLoginsFragmentDirections.actionSavedLoginsFragmentToLoginDetailFragment(login.guid)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +71,7 @@ class SavedLoginsControllerTest {
|
|||
fun `GIVEN the learn more option, WHEN handleLearnMoreClicked is called for it, then we should open the right support webpage`() {
|
||||
controller.handleLearnMoreClicked()
|
||||
|
||||
verify {
|
||||
verifyAll {
|
||||
browserNavigator.invoke(
|
||||
SupportUtils.getGenericSumoURLForTopic(SupportUtils.SumoTopic.SYNC_SETUP),
|
||||
true,
|
|
@ -16,6 +16,8 @@ import org.junit.Test
|
|||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor
|
||||
import org.mozilla.fenix.settings.logins.view.LoginsListViewHolder
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class LoginsListViewHolderTest {
|
||||
|
@ -39,7 +41,10 @@ class LoginsListViewHolderTest {
|
|||
|
||||
@Test
|
||||
fun `bind url and username`() {
|
||||
val holder = LoginsListViewHolder(view, interactor)
|
||||
val holder = LoginsListViewHolder(
|
||||
view,
|
||||
interactor
|
||||
)
|
||||
holder.bind(baseLogin)
|
||||
|
||||
assertEquals("mozilla.org", view.webAddressView.text)
|
||||
|
@ -48,7 +53,10 @@ class LoginsListViewHolderTest {
|
|||
|
||||
@Test
|
||||
fun `call interactor on click`() {
|
||||
val holder = LoginsListViewHolder(view, interactor)
|
||||
val holder = LoginsListViewHolder(
|
||||
view,
|
||||
interactor
|
||||
)
|
||||
holder.bind(baseLogin)
|
||||
|
||||
view.performClick()
|
||||
|
|
|
@ -7,15 +7,25 @@ package org.mozilla.fenix.settings.logins
|
|||
import io.mockk.mockk
|
||||
import io.mockk.verifyAll
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.settings.logins.controller.LoginsListController
|
||||
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
||||
import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor
|
||||
import kotlin.random.Random
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class SavedLoginsInteractorTest {
|
||||
private val controller: SavedLoginsController = mockk(relaxed = true)
|
||||
private val interactor = SavedLoginsInteractor(controller)
|
||||
private val listController: LoginsListController = mockk(relaxed = true)
|
||||
private val savedLoginsStorageController: SavedLoginsStorageController = mockk(relaxed = true)
|
||||
private lateinit var interactor: SavedLoginsInteractor
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
interactor = SavedLoginsInteractor(listController, savedLoginsStorageController)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a SavedLogin being clicked, WHEN the interactor is called for it, THEN it should just delegate the controller`() {
|
||||
|
@ -23,7 +33,7 @@ class SavedLoginsInteractorTest {
|
|||
interactor.onItemClicked(item)
|
||||
|
||||
verifyAll {
|
||||
controller.handleItemClicked(item)
|
||||
listController.handleItemClicked(item)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,7 +44,7 @@ class SavedLoginsInteractorTest {
|
|||
interactor.onSortingStrategyChanged(sortingStrategy)
|
||||
|
||||
verifyAll {
|
||||
controller.handleSort(sortingStrategy)
|
||||
listController.handleSort(sortingStrategy)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +53,13 @@ class SavedLoginsInteractorTest {
|
|||
interactor.onLearnMoreClicked()
|
||||
|
||||
verifyAll {
|
||||
controller.handleLearnMoreClicked()
|
||||
listController.handleLearnMoreClicked()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadAndMapLoginsTest() {
|
||||
interactor.loadAndMapLogins()
|
||||
verifyAll { savedLoginsStorageController.handleLoadAndMapLogins() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
/* 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.settings.logins
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Looper
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
import mozilla.components.concept.storage.Login
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.Components
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.LooperMode
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@LooperMode(LooperMode.Mode.PAUSED)
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class SavedLoginsStorageControllerTest {
|
||||
private lateinit var components: Components
|
||||
private val context: Context = mockk(relaxed = true)
|
||||
private lateinit var controller: SavedLoginsStorageController
|
||||
private val navController: NavController = mockk(relaxed = true)
|
||||
private val loginsFragmentStore: LoginsFragmentStore = mockk(relaxed = true)
|
||||
private val scope = TestCoroutineScope()
|
||||
private val loginMock: Login = mockk(relaxed = true)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
every { navController.currentDestination } returns NavDestination("").apply {
|
||||
id = R.id.loginDetailFragment
|
||||
}
|
||||
coEvery { context.components.core.passwordsStorage.get(any()) } returns loginMock
|
||||
every { loginsFragmentStore.dispatch(any()) } returns mockk()
|
||||
coEvery { context.components.core.passwordsStorage } returns mockk(relaxed = true)
|
||||
components = mockk(relaxed = true)
|
||||
|
||||
controller = SavedLoginsStorageController(
|
||||
context = context,
|
||||
viewLifecycleScope = MainScope(),
|
||||
navController = navController,
|
||||
loginsFragmentStore = loginsFragmentStore
|
||||
)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
scope.cleanupTestCoroutines()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN a login is deleted, THEN navigate back to the previous page`() = runBlocking {
|
||||
val loginId = "id"
|
||||
// mock for deleteLoginJob: Deferred<Boolean>?
|
||||
coEvery { context.components.core.passwordsStorage.delete(any()) } returns true
|
||||
controller.delete(loginId)
|
||||
|
||||
shadow()
|
||||
|
||||
coVerify { context.components.core.passwordsStorage.delete(loginId) }
|
||||
}
|
||||
|
||||
private fun shadow() {
|
||||
// solves issue with Roboelectric v4.3 and SDK 28
|
||||
// https://github.com/robolectric/robolectric/issues/5356
|
||||
shadowOf(Looper.getMainLooper()).idle()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN fetching the login list, THEN update the state in the store`() {
|
||||
val loginId = "id"
|
||||
// for deferredLogin: Deferred<List<Login>>?
|
||||
coEvery { context.components.core.passwordsStorage.list() } returns listOf()
|
||||
|
||||
controller.fetchLoginDetails(loginId)
|
||||
|
||||
coVerify { context.components.core.passwordsStorage.list() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN saving an update to an item, THEN navigate to login detail view`() {
|
||||
val login = Login(
|
||||
guid = "id",
|
||||
origin = "https://www.test.co.gov.org",
|
||||
username = "user123",
|
||||
password = "securePassword1",
|
||||
httpRealm = "httpRealm",
|
||||
formActionOrigin = ""
|
||||
)
|
||||
coEvery { context.components.core.passwordsStorage.get(any()) } returns loginMock
|
||||
|
||||
controller.save(login.guid!!, login.username, login.password)
|
||||
|
||||
coVerify { context.components.core.passwordsStorage.get(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN finding login dupes, THEN update duplicates in the store`() {
|
||||
val login = Login(
|
||||
guid = "id",
|
||||
origin = "https://www.test.co.gov.org",
|
||||
username = "user123",
|
||||
password = "securePassword1",
|
||||
httpRealm = "httpRealm",
|
||||
formActionOrigin = ""
|
||||
)
|
||||
|
||||
coEvery { context.components.core.passwordsStorage.get(any()) } returns login
|
||||
|
||||
// for deferredLogin: Deferred<List<Login>>?
|
||||
coEvery {
|
||||
context.components.core.passwordsStorage.getPotentialDupesIgnoringUsername(any())
|
||||
} returns listOf()
|
||||
|
||||
controller.findPotentialDuplicates(login.guid!!)
|
||||
|
||||
shadow()
|
||||
|
||||
coVerify {
|
||||
context.components.core.passwordsStorage.getPotentialDupesIgnoringUsername(login)
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue