Merge remote-tracking branch 'mozilla/master' into fork

pull/102/head
Abhijit Valluri 4 years ago
commit 48ea10cd9d

@ -422,6 +422,7 @@ dependencies {
implementation Deps.mozilla_feature_site_permissions
implementation Deps.mozilla_feature_readerview
implementation Deps.mozilla_feature_tab_collections
implementation Deps.mozilla_feature_recentlyclosed
implementation Deps.mozilla_feature_top_sites
implementation Deps.mozilla_feature_share
implementation Deps.mozilla_feature_accounts_push

@ -11,6 +11,7 @@ import okhttp3.mockwebserver.MockWebServer
import org.junit.Rule
import org.junit.Before
import org.junit.After
import org.junit.Ignore
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
@ -88,6 +89,7 @@ class SettingsAddonsTest {
}
}
@Ignore("Failing intermittently on Firebase: https://github.com/mozilla-mobile/fenix/issues/13829")
// Opens the addons settings menu, installs an addon, then uninstalls
@Test
fun verifyAddonsCanBeUninstalled() {

@ -32,5 +32,6 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromAddonDetailsFragment(R.id.addonDetailsFragment),
FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment),
FromLoginDetailFragment(R.id.loginDetailFragment),
FromTabTray(R.id.tabTrayDialogFragment)
FromTabTray(R.id.tabTrayDialogFragment),
FromRecentlyClosed(R.id.recentlyClosedFragment)
}

@ -83,6 +83,7 @@ import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.search.SearchFragmentDirections
@ -703,6 +704,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabTray ->
TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromRecentlyClosed ->
RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId)
}
/**

@ -771,20 +771,35 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
}
private fun initializeEngineView(toolbarHeight: Int) {
engineView.setDynamicToolbarMaxHeight(toolbarHeight)
val context = requireContext()
val behavior = when (context.settings().toolbarPosition) {
ToolbarPosition.BOTTOM -> EngineViewBottomBehavior(context, null)
ToolbarPosition.TOP -> SwipeRefreshScrollingViewBehavior(
context,
null,
engineView,
browserToolbarView
)
}
(swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior
if (context.settings().isDynamicToolbarEnabled) {
engineView.setDynamicToolbarMaxHeight(toolbarHeight)
val behavior = when (context.settings().toolbarPosition) {
// Set engineView dynamic vertical clipping depending on the toolbar position.
ToolbarPosition.BOTTOM -> EngineViewBottomBehavior(context, null)
// Set scroll flags depending on if if the browser or the website is doing the scroll.
ToolbarPosition.TOP -> SwipeRefreshScrollingViewBehavior(
context,
null,
engineView,
browserToolbarView
)
}
(swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior
} else {
// Ensure webpage's bottom elements are aligned to the very bottom of the engineView.
engineView.setDynamicToolbarMaxHeight(0)
// Effectively place the engineView on top of the toolbar if that is not dynamic.
if (context.settings().shouldUseBottomToolbar) {
val browserEngine = swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams
browserEngine.bottomMargin =
requireContext().resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
}
}
}
/**
@ -916,14 +931,13 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
*/
protected open fun removeSessionIfNeeded(): Boolean {
getSessionById()?.let { session ->
val sessionManager = requireComponents.core.sessionManager
return if (session.source == SessionState.Source.ACTION_VIEW) {
activity?.finish()
sessionManager.remove(session)
requireComponents.useCases.tabsUseCases.removeTab(session)
true
} else {
if (session.hasParentSession) {
sessionManager.remove(session, true)
requireComponents.useCases.tabsUseCases.removeTab(session)
}
// We want to return to home if this session didn't have a parent session to select.
val goToOverview = !session.hasParentSession
@ -1099,7 +1113,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
if (webAppToolbarShouldBeVisible) {
browserToolbarView.view.isVisible = true
val toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
engineView.setDynamicToolbarMaxHeight(toolbarHeight)
initializeEngineView(toolbarHeight)
}
}
}

@ -54,6 +54,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
private var readerModeAvailable = false
private var openInAppOnboardingObserver: OpenInAppOnboardingObserver? = null
override fun onCreateView(
inflater: LayoutInflater,
@ -159,15 +160,16 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
}
session?.register(toolbarSessionObserver, viewLifecycleOwner, autoPause = true)
if (settings.shouldShowOpenInAppBanner) {
session?.register(
OpenInAppOnboardingObserver(
context = context,
navController = findNavController(),
settings = settings,
appLinksUseCases = context.components.useCases.appLinksUseCases,
container = browserToolbarView.view.parent as ViewGroup
),
if (settings.shouldShowOpenInAppBanner && session != null) {
openInAppOnboardingObserver = OpenInAppOnboardingObserver(
context = context,
navController = findNavController(),
settings = settings,
appLinksUseCases = context.components.useCases.appLinksUseCases,
container = browserToolbarView.view.parent as ViewGroup
)
session.register(
openInAppOnboardingObserver!!,
owner = this,
autoPause = true
)
@ -188,6 +190,16 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
subscribeToTabCollections()
}
override fun onStop() {
super.onStop()
// This observer initialized in onStart has a reference to fragment's view.
// Prevent it leaking the view after the latter onDestroyView.
if (openInAppOnboardingObserver != null) {
getSessionById()?.unregister(openInAppOnboardingObserver!!)
openInAppOnboardingObserver = null
}
}
private fun subscribeToTabCollections() {
Observer<List<TabCollection>> {
requireComponents.core.tabCollectionStorage.cachedTabCollections = it

@ -4,9 +4,9 @@
package org.mozilla.fenix.components
import android.app.Application
import android.content.Context
import android.content.Intent
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import mozilla.components.feature.addons.AddonManager
import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
@ -120,7 +120,7 @@ class Components(private val context: Context) {
val migrationStore by lazy { MigrationStore() }
val performance by lazy { PerformanceComponent() }
val push by lazy { Push(context, analytics.crashReporter) }
val wifiConnectionMonitor by lazy { WifiConnectionMonitor(context.getSystemService()!!) }
val wifiConnectionMonitor by lazy { WifiConnectionMonitor(context as Application) }
val settings by lazy { Settings(context) }

@ -20,6 +20,7 @@ import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.engine.EngineMiddleware
import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
@ -27,6 +28,7 @@ import mozilla.components.browser.storage.sync.PlacesHistoryStorage
import mozilla.components.browser.storage.sync.RemoteTabsStorage
import mozilla.components.browser.thumbnails.ThumbnailsMiddleware
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
import mozilla.components.concept.base.crash.CrashReporting
import mozilla.components.concept.engine.DefaultSettings
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
@ -39,6 +41,7 @@ import mozilla.components.feature.media.middleware.MediaMiddleware
import mozilla.components.feature.pwa.ManifestStorage
import mozilla.components.feature.pwa.WebAppShortcutManager
import mozilla.components.feature.readerview.ReaderViewMiddleware
import mozilla.components.feature.recentlyclosed.RecentlyClosedMiddleware
import mozilla.components.feature.session.HistoryDelegate
import mozilla.components.feature.top.sites.DefaultTopSitesStorage
import mozilla.components.feature.top.sites.PinnedSiteStorage
@ -51,7 +54,6 @@ import mozilla.components.service.digitalassetlinks.RelationChecker
import mozilla.components.service.digitalassetlinks.local.StatementApi
import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker
import mozilla.components.service.sync.logins.SyncableLoginsStorage
import mozilla.components.support.base.crash.CrashReporting
import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.AppRequestInterceptor
import org.mozilla.fenix.Config
@ -140,12 +142,15 @@ class Core(private val context: Context, private val crashReporter: CrashReporti
val store by lazy {
BrowserStore(
middleware = listOf(
RecentlyClosedMiddleware(context, RECENTLY_CLOSED_MAX, engine),
MediaMiddleware(context, MediaService::class.java),
DownloadMiddleware(context, DownloadService::class.java),
ReaderViewMiddleware(),
ThumbnailsMiddleware(thumbnailStorage)
) + EngineMiddleware.create(engine, ::findSessionById)
)
).also {
it.dispatch(RecentlyClosedAction.InitializeRecentlyClosedState)
}
}
private fun findSessionById(tabId: String): Session? {
@ -344,7 +349,7 @@ class Core(private val context: Context, private val crashReporter: CrashReporti
fun getPreferredColorScheme(): PreferredColorScheme {
val inDark =
(context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES
Configuration.UI_MODE_NIGHT_YES
return when {
context.settings().shouldUseDarkTheme -> PreferredColorScheme.Dark
context.settings().shouldUseLightTheme -> PreferredColorScheme.Light
@ -357,5 +362,6 @@ class Core(private val context: Context, private val crashReporter: CrashReporti
private const val KEY_STRENGTH = 256
private const val KEY_STORAGE_NAME = "core_prefs"
private const val PASSWORDS_KEY = "passwords"
private const val RECENTLY_CLOSED_MAX = 5
}
}

@ -10,8 +10,8 @@ import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.lib.crash.CrashReporter
import mozilla.components.support.base.crash.Breadcrumb
/**
* Records breadcrumbs when the fragment changes.

@ -22,6 +22,7 @@ import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings
/**
* An interface that handles the view manipulation of the BrowserToolbar, triggered by the Interactor
@ -156,7 +157,9 @@ class DefaultBrowserToolbarController(
}
override fun handleScroll(offset: Int) {
engineView.setVerticalClipping(offset)
if (activity.settings().isDynamicToolbarEnabled) {
engineView.setVerticalClipping(offset)
}
}
companion object {

@ -232,8 +232,12 @@ class BrowserToolbarView(
fun setScrollFlags(shouldDisableScroll: Boolean = false) {
when (settings.toolbarPosition) {
ToolbarPosition.BOTTOM -> {
(view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
behavior = BrowserToolbarBottomBehavior(view.context, null)
if (settings.isDynamicToolbarEnabled) {
(view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
behavior = BrowserToolbarBottomBehavior(view.context, null)
}
} else {
expand()
}
}
ToolbarPosition.TOP -> {

@ -7,7 +7,7 @@ package org.mozilla.fenix.ext
import android.app.Activity
import android.view.View
import android.view.WindowManager
import mozilla.components.support.base.crash.Breadcrumb
import mozilla.components.concept.base.crash.Breadcrumb
/**
* Attempts to call immersive mode using the View to hide the status bar and navigation buttons.

@ -12,7 +12,7 @@ import androidx.navigation.NavDirections
import androidx.navigation.NavOptions
import androidx.navigation.fragment.NavHostFragment.findNavController
import androidx.navigation.fragment.findNavController
import mozilla.components.support.base.crash.Breadcrumb
import mozilla.components.concept.base.crash.Breadcrumb
import org.mozilla.fenix.NavHostActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components

@ -477,7 +477,7 @@ class HomeFragment : Fragment() {
.let { SessionManager.Snapshot(it, selectedIndex) }
tabs.forEach {
sessionManager.remove(it)
requireComponents.useCases.tabsUseCases.removeTab(it)
}
val snackbarMessage = if (sessionCode == ALL_PRIVATE_TABS) {
@ -505,7 +505,7 @@ class HomeFragment : Fragment() {
val isSelected =
session.id == requireComponents.core.store.state.selectedTabId ?: false
sessionManager.remove(session)
requireComponents.useCases.tabsUseCases.removeTab(sessionId)
val snackbarMessage = if (snapshot.session.private) {
requireContext().getString(R.string.snackbar_private_tab_closed)

@ -16,6 +16,7 @@ import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.feature.top.sites.TopSite.Type.DEFAULT
import mozilla.components.feature.top.sites.TopSite.Type.FRECENT
import mozilla.components.feature.top.sites.TopSite.Type.PINNED
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
@ -54,8 +55,14 @@ class TopSiteItemViewHolder(
}
fun bind(topSite: TopSite) {
this.topSite = topSite
top_site_title.text = topSite.title
pin_indicator.visibility = if (topSite.type == PINNED) {
View.VISIBLE
} else {
View.GONE
}
when (topSite.url) {
SupportUtils.POCKET_TRENDING_URL -> {
favicon_image.setImageDrawable(getDrawable(itemView.context, R.drawable.ic_pocket))
@ -64,6 +71,8 @@ class TopSiteItemViewHolder(
itemView.context.components.core.icons.loadIntoView(favicon_image, topSite.url)
}
}
this.topSite = topSite
}
private fun onTouchEvent(

@ -27,9 +27,9 @@ enum class HistoryItemTimeGroup {
}
}
class HistoryAdapter(
private val historyInteractor: HistoryInteractor
) : PagedListAdapter<HistoryItem, HistoryListItemViewHolder>(historyDiffCallback), SelectionHolder<HistoryItem> {
class HistoryAdapter(private val historyInteractor: HistoryInteractor) :
PagedListAdapter<HistoryItem, HistoryListItemViewHolder>(historyDiffCallback),
SelectionHolder<HistoryItem> {
private var mode: HistoryFragmentState.Mode = HistoryFragmentState.Mode.Normal
override val selectedItems get() = mode.selectedItems

@ -8,6 +8,7 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.res.Resources
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.concept.engine.prompt.ShareData
@ -15,6 +16,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar
@Suppress("TooManyFunctions")
interface HistoryController {
fun handleOpen(item: HistoryItem, mode: BrowsingMode? = null)
fun handleSelect(item: HistoryItem)
@ -26,8 +28,10 @@ interface HistoryController {
fun handleCopyUrl(item: HistoryItem)
fun handleShare(item: HistoryItem)
fun handleRequestSync()
fun handleEnterRecentlyClosed()
}
@Suppress("TooManyFunctions")
class DefaultHistoryController(
private val store: HistoryFragmentStore,
private val navController: NavController,
@ -101,4 +105,11 @@ class DefaultHistoryController(
store.dispatch(HistoryFragmentAction.FinishSync)
}
}
override fun handleEnterRecentlyClosed() {
navController.navigate(
HistoryFragmentDirections.actionGlobalRecentlyClosed(),
NavOptions.Builder().setPopUpTo(R.id.recentlyClosedFragment, true).build()
)
}
}

@ -25,6 +25,7 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.service.fxa.sync.SyncReason
@ -49,12 +50,15 @@ import org.mozilla.fenix.utils.allowUndo
@SuppressWarnings("TooManyFunctions", "LargeClass")
class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandler {
private lateinit var historyStore: HistoryFragmentStore
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
private var _historyView: HistoryView? = null
protected val historyView: HistoryView
get() = _historyView!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -91,7 +95,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
historyInteractor = HistoryInteractor(
historyController
)
historyView = HistoryView(view.historyLayout, historyInteractor)
_historyView = HistoryView(
view.historyLayout,
historyInteractor
)
return view
}
@ -234,6 +241,11 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
return historyView.onBackPressed()
}
override fun onDestroyView() {
super.onDestroyView()
_historyView = null
}
private fun openItem(item: HistoryItem, mode: BrowsingMode? = null) {
requireComponents.analytics.metrics.track(Event.HistoryItemOpened)
@ -255,8 +267,9 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
}
setPositiveButton(R.string.delete_browsing_data_prompt_allow) { dialog: DialogInterface, _ ->
historyStore.dispatch(HistoryFragmentAction.EnterDeletionMode)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycleScope.launch(IO) {
requireComponents.analytics.metrics.track(Event.HistoryAllItemsRemoved)
requireComponents.core.store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction)
requireComponents.core.historyStorage.deleteEverything()
launch(Main) {
viewModel.invalidate()

@ -61,4 +61,8 @@ class HistoryInteractor(
override fun onRequestSync() {
historyController.handleRequestSync()
}
override fun onRecentlyClosedClicked() {
historyController.handleEnterRecentlyClosed()
}
}

@ -77,6 +77,11 @@ interface HistoryViewInteractor : SelectionInteractor<HistoryItem> {
* Called when the user requests a sync of the history
*/
fun onRequestSync()
/**
* Called when the user clicks on recently closed tab button.
*/
fun onRecentlyClosedClicked()
}
/**

@ -5,10 +5,12 @@
package org.mozilla.fenix.library.history.viewholders
import android.view.View
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.history_list_item.view.*
import kotlinx.android.synthetic.main.library_site_item.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.library.SelectionHolder
@ -38,6 +40,10 @@ class HistoryListItemViewHolder(
historyInteractor.onDeleteSome(selected)
}
}
itemView.recently_closed.setOnClickListener {
historyInteractor.onRecentlyClosedClicked()
}
}
fun bind(
@ -56,7 +62,7 @@ class HistoryListItemViewHolder(
itemView.history_layout.titleView.text = item.title
itemView.history_layout.urlView.text = item.url
toggleDeleteButton(showDeleteButton, mode === HistoryFragmentState.Mode.Normal)
toggleTopContent(showDeleteButton, mode === HistoryFragmentState.Mode.Normal)
val headerText = timeGroup?.humanReadable(itemView.context)
toggleHeader(headerText)
@ -86,11 +92,11 @@ class HistoryListItemViewHolder(
}
}
private fun toggleDeleteButton(
showDeleteButton: Boolean,
private fun toggleTopContent(
showTopContent: Boolean,
isNormalMode: Boolean
) {
if (showDeleteButton) {
if (showTopContent) {
itemView.delete_button.run {
visibility = View.VISIBLE
@ -102,7 +108,16 @@ class HistoryListItemViewHolder(
alpha = DELETE_BUTTON_DISABLED_ALPHA
}
}
val numRecentTabs = itemView.context.components.core.store.state.closedTabs.size
itemView.recently_closed_tabs_description.text = String.format(
itemView.context.getString(
if (numRecentTabs == 1)
R.string.recently_closed_tab else R.string.recently_closed_tabs
), numRecentTabs
)
itemView.recently_closed.isVisible = true
} else {
itemView.recently_closed.visibility = View.GONE
itemView.delete_button.visibility = View.GONE
}
}

@ -0,0 +1,36 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.recentlyclosed
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import mozilla.components.browser.state.state.ClosedTab
class RecentlyClosedAdapter(
private val interactor: RecentlyClosedFragmentInteractor
) : ListAdapter<ClosedTab, RecentlyClosedItemViewHolder>(DiffCallback) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecentlyClosedItemViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(RecentlyClosedItemViewHolder.LAYOUT_ID, parent, false)
return RecentlyClosedItemViewHolder(view, interactor)
}
override fun onBindViewHolder(holder: RecentlyClosedItemViewHolder, position: Int) {
holder.bind(getItem(position))
}
private object DiffCallback : DiffUtil.ItemCallback<ClosedTab>() {
override fun areItemsTheSame(oldItem: ClosedTab, newItem: ClosedTab) =
oldItem.id == newItem.id || oldItem.title == newItem.title || oldItem.url == newItem.url
override fun areContentsTheSame(oldItem: ClosedTab, newItem: ClosedTab) =
oldItem.id == newItem.id || oldItem.title == newItem.title || oldItem.url == newItem.url
}
}

@ -0,0 +1,86 @@
/* 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.library.recentlyclosed
import android.content.ClipData
import android.content.ClipboardManager
import android.content.res.Resources
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.state.ClosedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.recentlyclosed.ext.restoreTab
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar
interface RecentlyClosedController {
fun handleOpen(item: ClosedTab, mode: BrowsingMode? = null)
fun handleDeleteOne(tab: ClosedTab)
fun handleCopyUrl(item: ClosedTab)
fun handleShare(item: ClosedTab)
fun handleNavigateToHistory()
fun handleRestore(item: ClosedTab)
}
class DefaultRecentlyClosedController(
private val navController: NavController,
private val store: BrowserStore,
private val sessionManager: SessionManager,
private val resources: Resources,
private val snackbar: FenixSnackbar,
private val clipboardManager: ClipboardManager,
private val activity: HomeActivity,
private val openToBrowser: (item: ClosedTab, mode: BrowsingMode?) -> Unit
) : RecentlyClosedController {
override fun handleOpen(item: ClosedTab, mode: BrowsingMode?) {
openToBrowser(item, mode)
}
override fun handleDeleteOne(tab: ClosedTab) {
store.dispatch(RecentlyClosedAction.RemoveClosedTabAction(tab))
}
override fun handleNavigateToHistory() {
navController.navigate(
RecentlyClosedFragmentDirections.actionGlobalHistoryFragment(),
NavOptions.Builder().setPopUpTo(R.id.historyFragment, true).build()
)
}
override fun handleCopyUrl(item: ClosedTab) {
val urlClipData = ClipData.newPlainText(item.url, item.url)
clipboardManager.setPrimaryClip(urlClipData)
with(snackbar) {
setText(resources.getString(R.string.url_copied))
show()
}
}
override fun handleShare(item: ClosedTab) {
navController.navigate(
RecentlyClosedFragmentDirections.actionGlobalShareFragment(
data = arrayOf(ShareData(url = item.url, title = item.title))
)
)
}
override fun handleRestore(item: ClosedTab) {
item.restoreTab(
store,
sessionManager,
onTabRestored = {
activity.openToBrowser(
from = BrowserDirection.FromRecentlyClosed
)
}
)
}
}

@ -0,0 +1,135 @@
/* 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.library.recentlyclosed
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import kotlinx.android.synthetic.main.fragment_recently_closed_tabs.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.state.ClosedTab
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.library.LibraryPageFragment
@Suppress("TooManyFunctions")
class RecentlyClosedFragment : LibraryPageFragment<ClosedTab>() {
private lateinit var recentlyClosedFragmentStore: RecentlyClosedFragmentStore
private var _recentlyClosedFragmentView: RecentlyClosedFragmentView? = null
protected val recentlyClosedFragmentView: RecentlyClosedFragmentView
get() = _recentlyClosedFragmentView!!
private lateinit var recentlyClosedInteractor: RecentlyClosedFragmentInteractor
override fun onResume() {
super.onResume()
showToolbar(getString(R.string.library_recently_closed_tabs))
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.library_menu, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.close_history -> {
close()
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_recently_closed_tabs, container, false)
recentlyClosedFragmentStore = StoreProvider.get(this) {
RecentlyClosedFragmentStore(
RecentlyClosedFragmentState(
items = listOf()
)
)
}
recentlyClosedInteractor = RecentlyClosedFragmentInteractor(
recentlyClosedController = DefaultRecentlyClosedController(
navController = findNavController(),
store = requireComponents.core.store,
activity = activity as HomeActivity,
sessionManager = requireComponents.core.sessionManager,
resources = requireContext().resources,
snackbar = FenixSnackbar.make(
view = requireActivity().getRootView()!!,
isDisplayedWithBrowserToolbar = true
),
clipboardManager = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager,
openToBrowser = ::openItem
)
)
_recentlyClosedFragmentView = RecentlyClosedFragmentView(
view.recentlyClosedLayout,
recentlyClosedInteractor
)
return view
}
override fun onDestroyView() {
super.onDestroyView()
_recentlyClosedFragmentView = null
}
private fun openItem(tab: ClosedTab, mode: BrowsingMode? = null) {
mode?.let { (activity as HomeActivity).browsingModeManager.mode = it }
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = tab.url,
newTab = true,
from = BrowserDirection.FromRecentlyClosed
)
}
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
consumeFrom(recentlyClosedFragmentStore) {
recentlyClosedFragmentView.update(it.items)
}
requireComponents.core.store.flowScoped(viewLifecycleOwner) { flow ->
flow.map { state -> state.closedTabs }
.ifChanged()
.collect { tabs ->
recentlyClosedFragmentStore.dispatch(
RecentlyClosedFragmentAction.Change(tabs)
)
}
}
}
override val selectedItems: Set<ClosedTab> = setOf()
}

@ -0,0 +1,44 @@
/* 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.library.recentlyclosed
import mozilla.components.browser.state.state.ClosedTab
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
/**
* Interactor for the recently closed screen
* Provides implementations for the RecentlyClosedInteractor
*/
class RecentlyClosedFragmentInteractor(
private val recentlyClosedController: RecentlyClosedController
) : RecentlyClosedInteractor {
override fun restore(item: ClosedTab) {
recentlyClosedController.handleRestore(item)
}
override fun onCopyPressed(item: ClosedTab) {
recentlyClosedController.handleCopyUrl(item)
}
override fun onSharePressed(item: ClosedTab) {
recentlyClosedController.handleShare(item)
}
override fun onOpenInNormalTab(item: ClosedTab) {
recentlyClosedController.handleOpen(item, BrowsingMode.Normal)
}
override fun onOpenInPrivateTab(item: ClosedTab) {
recentlyClosedController.handleOpen(item, BrowsingMode.Private)
}
override fun onDeleteOne(tab: ClosedTab) {
recentlyClosedController.handleDeleteOne(tab)
}
override fun onNavigateToHistory() {
recentlyClosedController.handleNavigateToHistory()
}
}

@ -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.library.recentlyclosed
import mozilla.components.browser.state.state.ClosedTab
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
/**
* The [Store] for holding the [RecentlyClosedFragmentState] and applying [RecentlyClosedFragmentAction]s.
*/
class RecentlyClosedFragmentStore(initialState: RecentlyClosedFragmentState) :
Store<RecentlyClosedFragmentState, RecentlyClosedFragmentAction>(
initialState,
::recentlyClosedStateReducer
)
/**
* Actions to dispatch through the `RecentlyClosedFragmentStore` to modify
* `RecentlyClosedFragmentState` through the reducer.
*/
sealed class RecentlyClosedFragmentAction : Action {
data class Change(val list: List<ClosedTab>) : RecentlyClosedFragmentAction()
}
/**
* The state for the Recently Closed Screen
* @property items List of recently closed tabs to display
*/
data class RecentlyClosedFragmentState(val items: List<ClosedTab> = emptyList()) : State
/**
* The RecentlyClosedFragmentState Reducer.
*/
private fun recentlyClosedStateReducer(
state: RecentlyClosedFragmentState,
action: RecentlyClosedFragmentAction
): RecentlyClosedFragmentState {
return when (action) {
is RecentlyClosedFragmentAction.Change -> state.copy(items = action.list)
}
}

@ -0,0 +1,110 @@
/* 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.library.recentlyclosed
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_recently_closed.*
import mozilla.components.browser.state.state.ClosedTab
import org.mozilla.fenix.R
interface RecentlyClosedInteractor {
/**
* Called when an item is tapped to restore it.
*
* @param item the tapped item to restore.
*/
fun restore(item: ClosedTab)
/**
* Called when the view more history option is tapped.
*/
fun onNavigateToHistory()
/**
* Copies the URL of a recently closed tab item to the copy-paste buffer.
*
* @param item the recently closed tab item to copy the URL from
*/
fun onCopyPressed(item: ClosedTab)
/**
* Opens the share sheet for a recently closed tab item.
*
* @param item the recently closed tab item to share
*/
fun onSharePressed(item: ClosedTab)
/**
* Opens a recently closed tab item in a new tab.
*
* @param item the recently closed tab item to open in a new tab
*/
fun onOpenInNormalTab(item: ClosedTab)
/**
* Opens a recently closed tab item in a private tab.
*
* @param item the recently closed tab item to open in a private tab
*/
fun onOpenInPrivateTab(item: ClosedTab)
/**
* Deletes one recently closed tab item.
*
* @param item the recently closed tab item to delete.
*/
fun onDeleteOne(tab: ClosedTab)
}
/**
* View that contains and configures the Recently Closed List
*/
class RecentlyClosedFragmentView(
container: ViewGroup,
private val interactor: RecentlyClosedFragmentInteractor
) : LayoutContainer {
override val containerView: ConstraintLayout = LayoutInflater.from(container.context)
.inflate(R.layout.component_recently_closed, container, true)
.findViewById(R.id.recently_closed_wrapper)
private val recentlyClosedAdapter: RecentlyClosedAdapter = RecentlyClosedAdapter(interactor)
init {
recently_closed_list.apply {
layoutManager = LinearLayoutManager(containerView.context)
adapter = recentlyClosedAdapter
}
view_more_history.apply {
titleView.text =
containerView.context.getString(R.string.recently_closed_show_full_history)
urlView.isVisible = false
overflowView.isVisible = false
iconView.background = null
iconView.setImageDrawable(
ContextCompat.getDrawable(
containerView.context,
R.drawable.ic_history
)
)
setOnClickListener {
interactor.onNavigateToHistory()
}
}
}
fun update(items: List<ClosedTab>) {
recently_closed_empty_view.isVisible = items.isEmpty()
recently_closed_list.isVisible = items.isNotEmpty()
recentlyClosedAdapter.submitList(items)
}
}

@ -0,0 +1,68 @@
/* 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.library.recentlyclosed
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.history_list_item.view.*
import mozilla.components.browser.state.state.ClosedTab
import org.mozilla.fenix.R
import org.mozilla.fenix.library.history.HistoryItemMenu
import org.mozilla.fenix.utils.Do
class RecentlyClosedItemViewHolder(
view: View,
private val recentlyClosedFragmentInteractor: RecentlyClosedFragmentInteractor
) : RecyclerView.ViewHolder(view) {
private var item: ClosedTab? = null
init {
setupMenu()
}
fun bind(
item: ClosedTab
) {
itemView.history_layout.titleView.text =
if (item.title.isNotEmpty()) item.title else item.url
itemView.history_layout.urlView.text = item.url
if (this.item?.url != item.url) {
itemView.history_layout.loadFavicon(item.url)
}
itemView.setOnClickListener {
recentlyClosedFragmentInteractor.restore(item)
}
this.item = item
}
private fun setupMenu() {
val historyMenu = HistoryItemMenu(itemView.context) {
val item = this.item ?: return@HistoryItemMenu
Do exhaustive when (it) {
HistoryItemMenu.Item.Copy -> recentlyClosedFragmentInteractor.onCopyPressed(item)
HistoryItemMenu.Item.Share -> recentlyClosedFragmentInteractor.onSharePressed(item)
HistoryItemMenu.Item.OpenInNewTab -> recentlyClosedFragmentInteractor.onOpenInNormalTab(
item
)
HistoryItemMenu.Item.OpenInPrivateTab -> recentlyClosedFragmentInteractor.onOpenInPrivateTab(
item
)
HistoryItemMenu.Item.Delete -> recentlyClosedFragmentInteractor.onDeleteOne(
item
)
}
}
itemView.history_layout.attachMenu(historyMenu.menuController)
}
companion object {
const val LAYOUT_ID = R.layout.history_list_item
}
}

@ -4,7 +4,13 @@
package org.mozilla.fenix.search
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.text.SpannableString
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.navigation.NavController
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
@ -29,6 +35,7 @@ import org.mozilla.fenix.utils.Settings
/**
* An interface that handles the view manipulation of the Search, triggered by the Interactor
*/
@Suppress("TooManyFunctions")
interface SearchController {
fun handleUrlCommitted(url: String)
fun handleEditingCancelled()
@ -40,6 +47,7 @@ interface SearchController {
fun handleExistingSessionSelected(session: Session)
fun handleExistingSessionSelected(tabId: String)
fun handleSearchShortcutsButtonClicked()
fun handleCameraPermissionsNeeded()
}
@Suppress("TooManyFunctions", "LongParameterList")
@ -194,4 +202,51 @@ class DefaultSearchController(
handleExistingSessionSelected(session)
}
}
/**
* Creates and shows an [AlertDialog] when camera permissions are needed.
*
* In versions above M, [AlertDialog.BUTTON_POSITIVE] takes the user to the app settings. This
* intent only exists in M and above. Below M, [AlertDialog.BUTTON_POSITIVE] routes to a SUMO
* help page to find the app settings.
*
* [AlertDialog.BUTTON_NEGATIVE] dismisses the dialog.
*/
override fun handleCameraPermissionsNeeded() {
val dialog = buildDialog()
dialog.show()
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun buildDialog(): AlertDialog.Builder {
return AlertDialog.Builder(activity).apply {
val spannableText = SpannableString(
activity.resources.getString(R.string.camera_permissions_needed_message)
)
setMessage(spannableText)
setNegativeButton(R.string.camera_permissions_needed_negative_button_text) {
dialog: DialogInterface, _ ->
dialog.cancel()
}
setPositiveButton(R.string.camera_permissions_needed_positive_button_text) {
dialog: DialogInterface, _ ->
val intent: Intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
} else {
SupportUtils.createCustomTabIntent(
activity,
SupportUtils.getSumoURLForTopic(
activity,
SupportUtils.SumoTopic.QR_CAMERA_ACCESS
)
)
}
val uri = Uri.fromParts("package", activity.packageName, null)
intent.data = uri
dialog.cancel()
activity.startActivity(intent)
}
create()
}
}
}

@ -26,6 +26,7 @@ import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.android.synthetic.main.fragment_search.view.*
import kotlinx.android.synthetic.main.search_suggestions_hint.view.*
@ -50,6 +51,7 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
@ -219,6 +221,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
setNegativeButton(R.string.qr_scanner_dialog_negative) { dialog: DialogInterface, _ ->
requireComponents.analytics.metrics.track(Event.QRScannerNavigationDenied)
dialog.cancel()
resetFocus()
}
setPositiveButton(R.string.qr_scanner_dialog_positive) { dialog: DialogInterface, _ ->
requireComponents.analytics.metrics.track(Event.QRScannerNavigationAllowed)
@ -229,6 +232,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
from = BrowserDirection.FromSearch
)
dialog.dismiss()
resetFocus()
}
create()
}.show()
@ -241,8 +245,19 @@ class SearchFragment : Fragment(), UserInteractionHandler {
view.search_scan_button.setOnClickListener {
toolbarView.view.clearFocus()
requireComponents.analytics.metrics.track(Event.QRScannerOpened)
qrFeature.get()?.scan(R.id.container)
val cameraPermissionsDenied = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(
getPreferenceKey(R.string.pref_key_camera_permissions),
false
)
if (cameraPermissionsDenied) {
searchInteractor.onCameraPermissionsNeeded()
} else {
requireComponents.analytics.metrics.track(Event.QRScannerOpened)
qrFeature.get()?.scan(R.id.container)
}
}
view.search_engines_shortcut_button.setOnClickListener {
@ -368,15 +383,19 @@ class SearchFragment : Fragment(), UserInteractionHandler {
override fun onBackPressed(): Boolean {
return when {
qrFeature.onBackPressed() -> {
toolbarView.view.edit.focus()
view?.search_scan_button?.isChecked = false
toolbarView.view.requestFocus()
resetFocus()
true
}
else -> false
}
}
private fun resetFocus() {
search_scan_button.isChecked = false
toolbarView.view.edit.focus()
toolbarView.view.requestFocus()
}
private fun updateSearchWithLabel(searchState: SearchFragmentState) {
search_engine_shortcut.visibility =
if (searchState.showSearchShortcuts) View.VISIBLE else View.GONE
@ -408,8 +427,16 @@ class SearchFragment : Fragment(), UserInteractionHandler {
context?.let { context: Context ->
if (context.isPermissionGranted(Manifest.permission.CAMERA)) {
permissionDidUpdate = true
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putBoolean(
getPreferenceKey(R.string.pref_key_camera_permissions), false
).apply()
} else {
view?.search_scan_button?.isChecked = false
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putBoolean(
getPreferenceKey(R.string.pref_key_camera_permissions), true
).apply()
resetFocus()
}
}
}

@ -13,6 +13,7 @@ import org.mozilla.fenix.search.toolbar.ToolbarInteractor
* Interactor for the search screen
* Provides implementations for the AwesomeBarView and ToolbarView
*/
@Suppress("TooManyFunctions")
class SearchInteractor(
private val searchController: SearchController
) : AwesomeBarInteractor, ToolbarInteractor {
@ -56,4 +57,8 @@ class SearchInteractor(
override fun onExistingSessionSelected(tabId: String) {
searchController.handleExistingSessionSelected(tabId)
}
fun onCameraPermissionsNeeded() {
searchController.handleCameraPermissionsNeeded()
}
}

@ -19,6 +19,7 @@ import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.content.res.resolveAttribute
import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.search.SearchFragmentState
/**
@ -110,12 +111,15 @@ class ToolbarView(
}
val engineForSpeculativeConnects = if (!isPrivate) engine else null
ToolbarAutocompleteFeature(
view,
engineForSpeculativeConnects
).apply {
addDomainProvider(ShippedDomainsProvider().also { it.initialize(view.context) })
historyStorage?.also(::addHistoryStorageProvider)
if (context.settings().shouldAutocompleteInAwesomebar) {
ToolbarAutocompleteFeature(
view,
engineForSpeculativeConnects
).apply {
addDomainProvider(ShippedDomainsProvider().also { it.initialize(view.context) })
historyStorage?.also(::addHistoryStorageProvider)
}
}
}

@ -4,7 +4,13 @@
package org.mozilla.fenix.searchdialog
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.text.SpannableString
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.navigation.NavController
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
@ -33,6 +39,7 @@ class SearchDialogController(
private val navController: NavController,
private val settings: Settings,
private val metrics: MetricController,
private val dismissDialog: () -> Unit,
private val clearToolbarFocus: () -> Unit
) : SearchController {
@ -45,12 +52,15 @@ class SearchDialogController(
activity.startActivity(Intent(activity, CrashListActivity::class.java))
}
"about:addons" -> {
val directions = SearchDialogFragmentDirections.actionGlobalAddonsManagementFragment()
val directions =
SearchDialogFragmentDirections.actionGlobalAddonsManagementFragment()
navController.navigateSafe(R.id.searchDialogFragment, directions)
}
"moz://a" -> openSearchOrUrl(SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.MANIFESTO))
else -> if (url.isNotBlank()) {
openSearchOrUrl(url)
} else {
dismissDialog()
}
}
}
@ -182,4 +192,50 @@ class SearchDialogController(
handleExistingSessionSelected(session)
}
}
/**
* Creates and shows an [AlertDialog] when camera permissions are needed.
*
* In versions above M, [AlertDialog.BUTTON_POSITIVE] takes the user to the app settings. This
* intent only exists in M and above. Below M, [AlertDialog.BUTTON_POSITIVE] routes to a SUMO
* help page to find the app settings.
*
* [AlertDialog.BUTTON_NEGATIVE] dismisses the dialog.
*/
override fun handleCameraPermissionsNeeded() {
val dialog = buildDialog()
dialog.show()
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun buildDialog(): AlertDialog.Builder {
return AlertDialog.Builder(activity).apply {
val spannableText = SpannableString(
activity.resources.getString(R.string.camera_permissions_needed_message)
)
setMessage(spannableText)
setNegativeButton(R.string.camera_permissions_needed_negative_button_text) { _, _ ->
dismissDialog()
}
setPositiveButton(R.string.camera_permissions_needed_positive_button_text) {
dialog: DialogInterface, _ ->
val intent: Intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
} else {
SupportUtils.createCustomTabIntent(
activity,
SupportUtils.getSumoURLForTopic(
activity,
SupportUtils.SumoTopic.QR_CAMERA_ACCESS
)
)
}
val uri = Uri.fromParts("package", activity.packageName, null)
intent.data = uri
dialog.cancel()
activity.startActivity(intent)
}
create()
}
}
}

@ -4,6 +4,7 @@
package org.mozilla.fenix.searchdialog
import android.Manifest
import android.app.Activity
import android.app.Dialog
import android.content.Context
@ -28,21 +29,20 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.fragment_search_dialog.*
import kotlinx.android.synthetic.main.fragment_search_dialog.fill_link_from_clipboard
import kotlinx.android.synthetic.main.fragment_search_dialog.pill_wrapper
import kotlinx.android.synthetic.main.fragment_search_dialog.qr_scan_button
import kotlinx.android.synthetic.main.fragment_search_dialog.toolbar
import kotlinx.android.synthetic.main.fragment_search_dialog.view.*
import kotlinx.android.synthetic.main.search_suggestions_hint.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.qr.QrFeature
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.content.hasCamera
import mozilla.components.support.ktx.android.content.isPermissionGranted
import mozilla.components.support.ktx.android.content.res.getSpanned
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.ui.autocomplete.InlineAutocompleteEditText
@ -54,6 +54,8 @@ import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.isKeyboardVisible
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.search.SearchFragmentAction
@ -81,12 +83,17 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
private val qrFeature = ViewBoundFeatureWrapper<QrFeature>()
private val speechIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
private var keyboardVisible: Boolean = false
override fun onStart() {
super.onStart()
// https://github.com/mozilla-mobile/fenix/issues/14279
// To prevent GeckoView from resizing we're going to change the softInputMode to not adjust
// the size of the window.
requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
if (keyboardVisible) {
toolbarView.view.edit.focus()
}
}
override fun onStop() {
@ -94,6 +101,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
// https://github.com/mozilla-mobile/fenix/issues/14279
// Let's reset back to the default behavior after we're done searching
requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
keyboardVisible = toolbarView.view.isKeyboardVisible()
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -117,6 +125,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
val args by navArgs<SearchDialogFragmentArgs>()
val view = inflater.inflate(R.layout.fragment_search_dialog, container, false)
val activity = requireActivity() as HomeActivity
val isPrivate = activity.browsingModeManager.mode.isPrivate
requireComponents.analytics.metrics.track(Event.InteractWithSearchURLArea)
@ -138,8 +147,9 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
navController = findNavController(),
settings = requireContext().settings(),
metrics = requireComponents.analytics.metrics,
dismissDialog = { dismissAllowingStateLoss() },
clearToolbarFocus = {
toolbarView.view.hideKeyboard()
toolbarView.view.hideKeyboardAndSave()
toolbarView.view.clearFocus()
}
)
@ -148,8 +158,8 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
toolbarView = ToolbarView(
requireContext(),
interactor,
null,
false,
historyStorageProvider(),
isPrivate,
view.toolbar,
requireComponents.core.engine
).also(::addSearchButton)
@ -164,7 +174,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
setShortcutsChangedListener(FenixSearchEngineProvider.PREF_FILE_SEARCH_ENGINES)
view.awesome_bar.setOnTouchListener { _, _ ->
view.hideKeyboard()
view.hideKeyboardAndSave()
false
}
@ -174,7 +184,6 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
.findViewById<InlineAutocompleteEditText>(R.id.mozac_browser_toolbar_edit_url_view)
urlView?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
val isPrivate = (requireActivity() as HomeActivity).browsingModeManager.mode.isPrivate
requireComponents.core.engine.speculativeCreateSession(isPrivate)
return view
@ -188,7 +197,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
setupConstraints(view)
search_wrapper.setOnClickListener {
it.hideKeyboard()
it.hideKeyboardAndSave()
dismissAllowingStateLoss()
}
@ -202,8 +211,22 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
if (!requireContext().hasCamera()) { return@setOnClickListener }
toolbarView.view.clearFocus()
requireComponents.analytics.metrics.track(Event.QRScannerOpened)
qrFeature.get()?.scan(R.id.search_wrapper)
val cameraPermissionsDenied =
PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
getPreferenceKey(R.string.pref_key_camera_permissions),
false
)
if (cameraPermissionsDenied) {
interactor.onCameraPermissionsNeeded()
resetFocus()
view.hideKeyboard()
toolbarView.view.requestFocus()
} else {
requireComponents.analytics.metrics.track(Event.QRScannerOpened)
qrFeature.get()?.scan(R.id.search_wrapper)
}
}
fill_link_from_clipboard.setOnClickListener {
@ -278,6 +301,19 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
}
}
override fun onResume() {
super.onResume()
resetFocus()
toolbarView.view.edit.focus()
}
override fun onPause() {
super.onPause()
qr_scan_button.isChecked = false
view?.hideKeyboard()
toolbarView.view.requestFocus()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
if (requestCode == VoiceSearchActivity.SPEECH_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
intent?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.first()?.also {
@ -291,19 +327,23 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
override fun onBackPressed(): Boolean {
return when {
qrFeature.onBackPressed() -> {
toolbarView.view.edit.focus()
view?.qr_scan_button?.isChecked = false
toolbarView.view.requestFocus()
resetFocus()
true
}
else -> {
view?.hideKeyboard()
view?.hideKeyboardAndSave()
dismissAllowingStateLoss()
true
}
}
}
private fun historyStorageProvider(): HistoryStorage? {
return if (requireContext().settings().shouldShowHistorySuggestions) {
requireComponents.core.historyStorage
} else null
}
private fun createQrFeature(): QrFeature {
return QrFeature(
requireContext(),
@ -342,6 +382,39 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
})
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
when (requestCode) {
REQUEST_CODE_CAMERA_PERMISSIONS -> qrFeature.withFeature {
context?.let { context: Context ->
it.onPermissionsResult(permissions, grantResults)
if (!context.isPermissionGranted(Manifest.permission.CAMERA)) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putBoolean(
getPreferenceKey(R.string.pref_key_camera_permissions), true
).apply()
resetFocus()
} else {
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putBoolean(
getPreferenceKey(R.string.pref_key_camera_permissions), false
).apply()
}
}
}
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
private fun resetFocus() {
qr_scan_button.isChecked = false
toolbarView.view.edit.focus()
toolbarView.view.requestFocus()
}
private fun setupConstraints(view: View) {
if (view.context.settings().toolbarPosition == ToolbarPosition.BOTTOM) {
ConstraintSet().apply {
@ -350,11 +423,12 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
clear(toolbar.id, TOP)
connect(toolbar.id, BOTTOM, PARENT_ID, BOTTOM)
clear(awesome_bar.id, TOP)
clear(pill_wrapper.id, BOTTOM)
connect(awesome_bar.id, TOP, PARENT_ID, TOP)
connect(pill_wrapper.id, BOTTOM, toolbar.id, TOP)
clear(search_suggestions_hint.id, TOP)
connect(search_suggestions_hint.id, TOP, PARENT_ID, TOP)
clear(fill_link_from_clipboard.id, TOP)
connect(fill_link_from_clipboard.id, BOTTOM, pill_wrapper.id, TOP)
@ -386,6 +460,15 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
)
}
/**
* Used to save keyboard status on stop/sleep, to be restored later.
* See #14559
* */
private fun View.hideKeyboardAndSave() {
keyboardVisible = false
this.hideKeyboard()
}
private fun launchVoiceSearch() {
// Note if a user disables speech while the app is on the search fragment
// the voice button will still be available and *will* cause a crash if tapped,

@ -4,21 +4,28 @@
package org.mozilla.fenix.settings
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.VibrationEffect
import android.os.Vibrator
import android.provider.Settings
import android.text.SpannableString
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.NavHostFragment.findNavController
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import mozilla.components.feature.qr.QrFeature
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.showToolbar
@ -63,8 +70,18 @@ class PairFragment : Fragment(R.layout.fragment_pair), UserInteractionHandler {
view = view
)
val cameraPermissionsDenied = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(
getPreferenceKey(R.string.pref_key_camera_permissions),
false
)
qrFeature.withFeature {
it.scan(R.id.pair_layout)
if (cameraPermissionsDenied) {
showPermissionsNeededDialog()
} else {
it.scan(R.id.pair_layout)
}
}
}
@ -99,10 +116,57 @@ class PairFragment : Fragment(R.layout.fragment_pair), UserInteractionHandler {
qrFeature.withFeature {
it.onPermissionsResult(permissions, grantResults)
}
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putBoolean(
getPreferenceKey(R.string.pref_key_camera_permissions), false
).apply()
} else {
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putBoolean(
getPreferenceKey(R.string.pref_key_camera_permissions), true
).apply()
findNavController().popBackStack(R.id.turnOnSyncFragment, false)
}
}
}
}
/**
* Shows an [AlertDialog] when camera permissions are needed.
*
* In versions above M, [AlertDialog.BUTTON_POSITIVE] takes the user to the app settings. This
* intent only exists in M and above. Below M, [AlertDialog.BUTTON_POSITIVE] routes to a SUMO
* help page to find the app settings.
*
* [AlertDialog.BUTTON_NEGATIVE] dismisses the dialog.
*/
private fun showPermissionsNeededDialog() {
AlertDialog.Builder(requireContext()).apply {
val spannableText = SpannableString(
resources.getString(R.string.camera_permissions_needed_message)
)
setMessage(spannableText)
setNegativeButton(R.string.camera_permissions_needed_negative_button_text) {
dialog: DialogInterface, _ ->
dialog.cancel()
}
setPositiveButton(R.string.camera_permissions_needed_positive_button_text) {
dialog: DialogInterface, _ ->
val intent: Intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
} else {
SupportUtils.createCustomTabIntent(
requireContext(),
SupportUtils.getSumoURLForTopic(
requireContext(),
SupportUtils.SumoTopic.QR_CAMERA_ACCESS
)
)
}
dialog.cancel()
startActivity(intent)
}
create()
}.show()
}
}

@ -40,7 +40,8 @@ object SupportUtils {
SEARCH_SUGGESTION("how-search-firefox-preview"),
CUSTOM_SEARCH_ENGINES("custom-search-engines"),
UPGRADE_FAQ("firefox-preview-upgrade-faqs"),
SYNC_SETUP("how-set-firefox-sync-firefox-preview")
SYNC_SETUP("how-set-firefox-sync-firefox-preview"),
QR_CAMERA_ACCESS("qr-camera-access")
}
enum class MozillaPage(internal val path: String) {

@ -26,6 +26,7 @@ fun deleteAndQuit(activity: Activity, coroutineScope: CoroutineScope, snackbar:
activity.components.useCases.tabsUseCases.removeAllTabs,
activity.components.core.historyStorage,
activity.components.core.permissionStorage,
activity.components.core.store,
activity.components.core.icons,
activity.components.core.engine,
coroutineContext

@ -7,6 +7,8 @@ package org.mozilla.fenix.settings.deletebrowsingdata
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.tabs.TabsUseCases
@ -25,6 +27,7 @@ class DefaultDeleteBrowsingDataController(
private val removeAllTabs: TabsUseCases.RemoveAllTabsUseCase,
private val historyStorage: HistoryStorage,
private val permissionStorage: PermissionStorage,
private val store: BrowserStore,
private val iconsStorage: BrowserIcons,
private val engine: Engine,
private val coroutineContext: CoroutineContext = Dispatchers.Main
@ -41,6 +44,7 @@ class DefaultDeleteBrowsingDataController(
engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES))
historyStorage.deleteEverything()
iconsStorage.clear()
store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction)
}
}

@ -28,7 +28,6 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
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.ext.showToolbar
@ -45,11 +44,12 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da
super.onViewCreated(view, savedInstanceState)
controller = DefaultDeleteBrowsingDataController(
requireContext().components.useCases.tabsUseCases.removeAllTabs,
requireContext().components.core.historyStorage,
requireContext().components.core.permissionStorage,
requireContext().components.core.icons,
requireContext().components.core.engine
requireComponents.useCases.tabsUseCases.removeAllTabs,
requireComponents.core.historyStorage,
requireComponents.core.permissionStorage,
requireComponents.core.store,
requireComponents.core.icons,
requireComponents.core.engine
)
settings = requireContext().settings()

@ -32,6 +32,11 @@ class SearchEngineFragment : PreferenceFragmentCompat() {
isChecked = context.settings().shouldShowSearchSuggestions
}
val autocompleteURLsPreference =
requirePreference<SwitchPreference>(R.string.pref_key_enable_autocomplete_urls).apply {
isChecked = context.settings().shouldAutocompleteInAwesomebar
}
val searchSuggestionsInPrivatePreference =
requirePreference<CheckBoxPreference>(R.string.pref_key_show_search_suggestions_in_private).apply {
isChecked = context.settings().shouldShowSearchSuggestionsInPrivate
@ -73,6 +78,7 @@ class SearchEngineFragment : PreferenceFragmentCompat() {
showClipboardSuggestions.onPreferenceChangeListener = SharedPreferenceUpdater()
searchSuggestionsInPrivatePreference.onPreferenceChangeListener = SharedPreferenceUpdater()
showVoiceSearchPreference.onPreferenceChangeListener = SharedPreferenceUpdater()
autocompleteURLsPreference.onPreferenceChangeListener = SharedPreferenceUpdater()
searchSuggestionsPreference.setOnPreferenceClickListener {
if (!searchSuggestionsPreference.isChecked) {

@ -9,8 +9,7 @@ import androidx.navigation.NavController
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.storage.sync.Tab as SyncTab
import mozilla.components.concept.engine.profiler.Profiler
import mozilla.components.concept.base.profiler.Profiler
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.tabstray.Tab
import mozilla.components.feature.tabs.TabsUseCases
@ -21,6 +20,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.home.HomeFragment
import mozilla.components.browser.storage.sync.Tab as SyncTab
/**
* [TabTrayDialogFragment] controller.
@ -42,6 +42,7 @@ interface TabTrayController {
fun handleRemoveSelectedTab(tab: Tab)
fun handleOpenTab(tab: Tab)
fun handleEnterMultiselect()
fun handleRecentlyClosedClicked()
}
/**
@ -178,4 +179,9 @@ class DefaultTabTrayController(
override fun handleEnterMultiselect() {
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.EnterMultiSelectMode)
}
override fun handleRecentlyClosedClicked() {
val directions = TabTrayDialogFragmentDirections.actionGlobalRecentlyClosed()
navController.navigate(directions)
}
}

@ -73,6 +73,11 @@ interface TabTrayInteractor {
* Called when multiselect mode should be entered with no tabs selected.
*/
fun onEnterMultiselect()
/**
* Called when user clicks the recently closed tabs menu button.
*/
fun onOpenRecentlyClosedClicked()
}
/**
@ -92,6 +97,10 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab
controller.handleTabSettingsClicked()
}
override fun onOpenRecentlyClosedClicked() {
controller.handleRecentlyClosedClicked()
}
override fun onShareTabsClicked(private: Boolean) {
controller.onShareTabsClicked(private)
}

@ -302,6 +302,7 @@ class TabTrayView(
is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked(
isPrivateModeSelected
)
is TabTrayItemMenu.Item.OpenRecentlyClosed -> interactor.onOpenRecentlyClosedClicked()
}
}
@ -747,6 +748,7 @@ class TabTrayItemMenu(
object OpenTabSettings : Item()
object SaveToCollection : Item()
object CloseAllTabs : Item()
object OpenRecentlyClosed : Item()
}
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
@ -776,6 +778,13 @@ class TabTrayItemMenu(
onItemTapped.invoke(Item.OpenTabSettings)
},
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_recently_closed),
textColorResource = R.color.primary_text_normal_theme
) {
onItemTapped.invoke(Item.OpenRecentlyClosed)
},
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_close),
textColorResource = R.color.primary_text_normal_theme

@ -621,6 +621,11 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = true
)
val shouldAutocompleteInAwesomebar by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_enable_autocomplete_urls),
default = true
)
var defaultTopSitesAdded by booleanPreference(
appContext.getPreferenceKey(R.string.default_top_sites_added),
default = false

@ -18,21 +18,29 @@ import org.mozilla.fenix.utils.Settings
class SitePermissionsWifiIntegration(
private val settings: Settings,
private val wifiConnectionMonitor: WifiConnectionMonitor
) : LifecycleAwareFeature, WifiConnectionMonitor.Observer {
) : LifecycleAwareFeature {
/**
* Adds listener for autoplay setting [AUTOPLAY_ALLOW_ON_WIFI]. Sets all autoplay to allowed when
* WIFI is connected, blocked otherwise.
*/
override fun onWifiConnectionChanged(connected: Boolean) {
val setting =
if (connected) SitePermissionsRules.Action.ALLOWED else SitePermissionsRules.Action.BLOCKED
if (settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) == AUTOPLAY_ALLOW_ON_WIFI) {
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.AUTOPLAY_AUDIBLE, setting)
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.AUTOPLAY_INAUDIBLE, setting)
} else {
// The autoplay setting has changed, we can remove the listener
removeWifiConnectedListener()
private val wifiConnectedListener: ((Boolean) -> Unit) by lazy {
{ connected: Boolean ->
val setting =
if (connected) SitePermissionsRules.Action.ALLOWED else SitePermissionsRules.Action.BLOCKED
if (settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) == AUTOPLAY_ALLOW_ON_WIFI) {
settings.setSitePermissionsPhoneFeatureAction(
PhoneFeature.AUTOPLAY_AUDIBLE,
setting
)
settings.setSitePermissionsPhoneFeatureAction(
PhoneFeature.AUTOPLAY_INAUDIBLE,
setting
)
} else {
// The autoplay setting has changed, we can remove the listener
removeWifiConnectedListener()
}
}
}
@ -47,11 +55,11 @@ class SitePermissionsWifiIntegration(
}
fun addWifiConnectedListener() {
wifiConnectionMonitor.register(this)
wifiConnectionMonitor.addOnWifiConnectedChangedListener(wifiConnectedListener)
}
fun removeWifiConnectedListener() {
wifiConnectionMonitor.unregister(this)
wifiConnectionMonitor.removeOnWifiConnectedChangedListener(wifiConnectedListener)
}
// Until https://bugzilla.mozilla.org/show_bug.cgi?id=1621825 is fixed, AUTOPLAY_ALLOW_ALL

@ -5,12 +5,11 @@
package org.mozilla.fenix.wifi
import android.app.Application
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import mozilla.components.support.base.observer.Observable
import mozilla.components.support.base.observer.ObserverRegistry
/**
* Attaches itself to the [Application] and listens for WIFI available/not available events. This
@ -26,28 +25,30 @@ import mozilla.components.support.base.observer.ObserverRegistry
* app.components.wifiConnectionListener.start()
* ```
*/
class WifiConnectionMonitor(
private val connectivityManager: ConnectivityManager
) : Observable<WifiConnectionMonitor.Observer> by ObserverRegistry() {
class WifiConnectionMonitor(app: Application) {
private val callbacks = mutableSetOf<(Boolean) -> Unit>()
private val connectivityManager = app.getSystemService(Context.CONNECTIVITY_SERVICE) as
ConnectivityManager
private var callbackReceived: Boolean = false
private var lastKnownStateWasAvailable: Boolean? = null
private var isRegistered = false
private val frameworkListener = object : ConnectivityManager.NetworkCallback() {
override fun onLost(network: Network?) {
notifyAtLeastOneObserver { onWifiConnectionChanged(connected = false) }
callbackReceived = true
callbacks.forEach { it(false) }
lastKnownStateWasAvailable = false
}
override fun onAvailable(network: Network?) {
notifyAtLeastOneObserver { onWifiConnectionChanged(connected = true) }
callbackReceived = true
callbacks.forEach { it(true) }
lastKnownStateWasAvailable = true
}
}
/**
* Attaches the [WifiConnectionMonitor] to the application. After this has been called, callbacks
* added via [register] will be called until either the app exits, or [stop] is called.
* added via [addOnWifiConnectedChangedListener] will be called until either the app exits, or
* [stop] is called.
*
* Any existing callbacks will be called with the current state when this is called.
*/
@ -61,8 +62,10 @@ class WifiConnectionMonitor(
// AFAICT, the framework does not send an event when a new NetworkCallback is registered
// while the WIFI is not connected, so we push this manually. If the WIFI is on, it will send
// a follow up event shortly
if (!callbackReceived) {
notifyAtLeastOneObserver { onWifiConnectionChanged(connected = false) }
val noCallbacksReceivedYet = lastKnownStateWasAvailable == null
if (noCallbacksReceivedYet) {
lastKnownStateWasAvailable = false
callbacks.forEach { it(false) }
}
connectivityManager.registerNetworkCallback(request, frameworkListener)
@ -71,7 +74,7 @@ class WifiConnectionMonitor(
/**
* Detatches the [WifiConnectionMonitor] from the app. No callbacks added via
* [register] will be called after this has been called.
* [addOnWifiConnectedChangedListener] will be called after this has been called.
*/
fun stop() {
// Framework code will throw if an unregistered listener attempts to unregister.
@ -80,7 +83,25 @@ class WifiConnectionMonitor(
isRegistered = false
}
interface Observer {
fun onWifiConnectionChanged(connected: Boolean)
/**
* Adds [onWifiChanged] to a list of listeners that will be called whenever WIFI connects or
* disconnects.
*
* If [onWifiChanged] is successfully added (i.e., it is a new listener), it will be immediately
* called with the last known state.
*/
fun addOnWifiConnectedChangedListener(onWifiChanged: (Boolean) -> Unit) {
val lastKnownState = lastKnownStateWasAvailable
if (callbacks.add(onWifiChanged) && lastKnownState != null) {
onWifiChanged(lastKnownState)
}
}
/**
* Removes [onWifiChanged] from the list of listeners to be called whenever WIFI connects or
* disconnects.
*/
fun removeOnWifiConnectedChangedListener(onWifiChanged: (Boolean) -> Unit) {
callbacks.remove(onWifiChanged)
}
}

@ -0,0 +1,13 @@
<?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/. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M20.207 18.793L15.914 14.5l3.043-3.043a1 1 0 0 0 0-1.414A5.234 5.234 0 0 0 15.232 8.5h-0.214a3.269 3.269 0 0 1-3.268-3.268V4.5a1 1 0 0 0-1.707-0.707l-6.25 6.25A1 1 0 0 0 4.5 11.75h0.732A3.269 3.269 0 0 1 8.5 15.018v0.211A4.8 4.8 0 0 0 10.087 19a1 1 0 0 0 1.37-0.041l3.043-3.045 4.293 4.293a1 1 0 0 0 1.414-1.414z"
android:fillColor="?mozac_widget_favicon_border_color"/>
</vector>

@ -0,0 +1,38 @@
<?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/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/recently_closed_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.mozilla.fenix.library.LibrarySiteItemView
android:id="@+id/view_more_history"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recently_closed_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/view_more_history"
tools:listitem="@layout/history_list_item" />
<TextView
android:id="@+id/recently_closed_empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/recently_closed_empty_message"
android:textColor="?secondaryText"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -138,6 +138,7 @@
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow"
app:layout_constraintTop_toTopOf="@id/tab_layout"
app:tint="@color/primary_text_normal_theme"
app:srcCompat="@drawable/ic_new" />
<ImageButton

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recentlyClosedLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />

@ -42,8 +42,7 @@
app:layout_constraintBottom_toTopOf="@+id/pill_wrapper"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar"
mozac:awesomeBarChipBackgroundColor="@color/photonGreen50"
app:layout_constraintTop_toBottomOf="@id/top_barrier"
mozac:awesomeBarDescriptionTextColor="?secondaryText"
mozac:awesomeBarTitleTextColor="?primaryText" />
@ -55,7 +54,15 @@
android:layout="@layout/search_suggestions_hint"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/top_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="search_suggestions_hint"/>
<View
android:id="@+id/search_suggestions_hint_divider"

@ -2,12 +2,12 @@
<!-- 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:app="http://schemas.android.com/apk/res-auto"
android:layout_height="wrap_content"
android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/delete_button"
@ -16,18 +16,76 @@
android:text="@string/history_delete_all"
android:visibility="gone" />
<TextView
android:id="@+id/header_title"
android:layout_width="wrap_content"
<androidx.constraintlayout.widget.ConstraintLayout
android:visibility="gone"
android:id="@+id/recently_closed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="?android:attr/selectableItemBackground"
android:minHeight="@dimen/library_item_height">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="@dimen/history_favicon_width_height"
android:layout_height="@dimen/history_favicon_width_height"
android:layout_marginStart="20dp"
android:importantForAccessibility="no"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_multiple_tabs" />
<TextView
android:id="@+id/recently_closed_tabs_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="14sp"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/library_recently_closed_tabs"
android:textAlignment="viewStart"
android:textColor="?primaryText"
android:fontFamily="@font/metropolis_semibold"
android:paddingStart="20dp"
android:paddingEnd="0dp"
android:layout_marginBottom="8dp"
tools:text="Header"
android:visibility="gone" />
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@+id/recently_closed_tabs_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:layout_goneMarginEnd="@dimen/library_item_icon_margin_horizontal" />
<TextView
android:id="@+id/recently_closed_tabs_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?secondaryText"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toBottomOf="@id/recently_closed_tabs_header"
app:layout_goneMarginEnd="@dimen/library_item_icon_margin_horizontal"
tools:text="2 tabs" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:fontFamily="@font/metropolis_semibold"
android:paddingStart="20dp"
android:paddingEnd="0dp"
android:textColor="?primaryText"
android:textSize="14sp"
android:visibility="gone"
tools:text="Header" />
<org.mozilla.fenix.library.LibrarySiteItemView
android:id="@+id/history_layout"
@ -35,4 +93,3 @@
android:layout_height="wrap_content"
android:minHeight="@dimen/library_item_height" />
</LinearLayout>

@ -14,8 +14,8 @@
android:focusable="true">
<RadioButton
android:id="@+id/radio_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="@dimen/search_engine_radio_button_height"
android:layout_height="@dimen/search_engine_radio_button_height"
android:textAppearance="?android:attr/textAppearanceListItem"
android:layout_marginStart="@dimen/search_bar_search_engine_icon_padding"
android:layout_gravity="center" />
@ -24,7 +24,7 @@
android:importantForAccessibility="no"
android:layout_width="@dimen/search_engine_engine_icon_height"
android:layout_height="@dimen/search_engine_engine_icon_height"
android:layout_marginStart="@dimen/search_bar_search_icon_margin"
android:layout_marginStart="@dimen/search_engine_engine_icon_margin"
android:layout_gravity="center" />
<TextView
android:id="@+id/engine_text"

@ -6,6 +6,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/foundation"
android:paddingTop="20dp"
android:paddingEnd="20dp"
android:paddingStart="20dp"

@ -2,30 +2,57 @@
<!-- 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"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/top_site_item"
android:layout_width="match_parent"
android:layout_height="@dimen/top_sites_item_size"
android:layout_marginBottom="@dimen/top_sites_item_margin_top"
android:layout_marginTop="@dimen/top_sites_item_margin_top"
android:layout_marginBottom="@dimen/top_sites_item_margin_bottom"
android:orientation="vertical">
<ImageView
android:id="@+id/favicon_image"
style="@style/TopSite.Favicon"
android:layout_gravity="center"
android:importantForAccessibility="no" />
android:importantForAccessibility="no"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/top_site_title"
android:layout_width="64dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/top_sites_text_margin_top"
android:gravity="center"
android:singleLine="true"
android:layout_gravity="center_horizontal"
android:textColor="@color/top_site_title_text"
android:textSize="10sp"
android:layout_marginTop="@dimen/top_sites_text_margin_top" />
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/favicon_image"
tools:ignore="SmallSp" />
<FrameLayout
android:id="@+id/pin_indicator"
android:layout_width="16dp"
android:layout_height="16dp"
android:elevation="5dp"
android:translationX="8dp"
android:translationY="-8dp"
android:visibility="gone"
app:layout_constraintRight_toLeftOf="@id/favicon_image"
app:layout_constraintTop_toTopOf="@id/favicon_image">
<View
android:id="@+id/pin_icon"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_gravity="center"
android:background="@drawable/ic_pin" />
</FrameLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -31,6 +31,10 @@
android:id="@+id/action_global_search_dialog"
app:destination="@id/searchDialogFragment" />
<action
android:id="@+id/action_global_recently_closed"
app:destination="@id/recentlyClosedFragment" />
<action
android:id="@+id/action_global_shareFragment"
app:destination="@id/shareFragment" />
@ -177,6 +181,11 @@
app:argType="org.mozilla.fenix.components.metrics.Event$PerformedSearch$SearchAccessPoint" />
</fragment>
<fragment
android:id="@+id/recentlyClosedFragment"
android:name="org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragment"
android:label="@string/library_recently_closed_tabs" />
<fragment
android:id="@+id/SitePermissionsManagePhoneFeature"
android:name="org.mozilla.fenix.settings.sitepermissions.SitePermissionsManagePhoneFeatureFragment"

@ -263,6 +263,8 @@
<string name="preferences_open_links_in_a_private_tab">Abrir los enllaces nuna llingüeta privada</string>
<!-- Preference for allowing screenshots to be taken while in a private tab-->
<string name="preferences_allow_screenshots_in_private_mode">Permitir la fechura de captures nel restolar en privao</string>
<!-- Will inform the user of the risk of activating Allow screenshots in private browsing option -->
<string name="preferences_screenshots_in_private_mode_disclaimer">Si s\'activa, les llingüetes privaes tamién van ser visibles cuando s\'abran munches aplicaciones</string>
<!-- Preference for adding private browsing shortcut -->
<string name="preferences_add_private_browsing_shortcut">Amestar un atayu pa restolar en privao</string>
<!-- Preference for accessibility -->
@ -281,6 +283,8 @@
<string name="preferences_theme">Estilu</string>
<!-- Preference for customizing the home screen -->
<string name="preferences_home">Aniciu</string>
<!-- Preference for gestures based actions -->
<string name="preferences_gestures">Xestos</string>
<!-- Preference for settings related to visual options -->
<string name="preferences_customize">Personalización</string>
<!-- Preference description for banner about signing in -->
@ -1228,7 +1232,7 @@
<string name="add_to_homescreen_text_placeholder">Nome del atayu</string>
<!-- Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Pues amestar fácilmente esti sitiu web a la pantalla d\'aniciu del preséu p\'acceder nel intre y restolalu como si fore una aplicación.</string>
<string name="add_to_homescreen_description_2">Pues amestar fácilmente esti sitiu web a la pantalla d\'Aniciu del preséu p\'acceder aína a elli como si fore una aplicación nativa.</string>
<!-- Preference for managing the settings for logins and passwords in Fenix -->
<string name="preferences_passwords_logins_and_passwords">Anicios de sesión y contraseñes</string>
@ -1462,8 +1466,6 @@
<!-- Top Sites -->
<!-- Title text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_title">Algamóse la llende de sitios destacaos</string>
<!-- Content description text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_content">P\'amestar sitios nuevos desanicia dalgún. Ten primíu un sitiu y esbilla Desaniciar.</string>
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">Val, entendílo</string>
@ -1478,4 +1480,7 @@
<string name="no_collections_header1">Coleiciona les coses que t\'importen</string>
<!-- Deprecated: Label to describe what collections are to a new user without any collections -->
<string name="no_collections_description1">Agrupa busques, sitios y llingüetes asemeyaos p\'acceder aína a ellos dempués.</string>
<!-- Deprecated: Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Pues amestar fácilmente esti sitiu web a la pantalla d\'aniciu del preséu p\'acceder nel intre y restolalu como si fore una aplicación.</string>
</resources>

@ -402,6 +402,8 @@
<!-- Title for experiments preferences -->
<string name="preference_experiments">Доследы</string>
<!-- Summary for experiments preferences -->
<string name="preference_experiments_summary">Дазваляе Mozilla ўсталёўваць эксперыментальныя функцыі і збіраць звесткі для іх</string>
<!-- Preference switch for crash reporter -->
<string name="preferences_crash_reporter">Паведамляльнік пра крахі</string>
<!-- Preference switch for Mozilla location service -->
@ -934,6 +936,9 @@
<!-- Title for Accessibility Text Automatic Size Scaling Preference -->
<string name="preference_accessibility_auto_size_2">Аўтаматычны памер шрыфту</string>
<!-- Summary for Accessibility Text Automatic Size Scaling Preference -->
<string name="preference_accessibility_auto_size_summary">Памер шрыфту будзе адпавядаць наладам Android. Адключыце, каб кіраваць шрыфтам тут.</string>
<!-- Title for the Delete browsing data preference -->
<string name="preferences_delete_browsing_data">Выдаліць дадзеныя аглядання</string>
<!-- Title for the tabs item in Delete browsing data -->
@ -1184,6 +1189,8 @@
<string name="etp_cryptominers_title">Майнеры крыптавалют</string>
<!-- Category of trackers (fingerprinters) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_fingerprinters_title">Збіральнікі лічбавых адбіткаў</string>
<!-- Description of fingerprinters that can be blocked by Enhanced Tracking Protection -->
<string name="etp_fingerprinters_description">Спыняе збор унікальнай ідэнтыфікацыйнай інфармацыі пра вашу прыладу, якая можа быць выкарыстана для сачэння.</string>
<!-- Category of trackers (tracking content) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_tracking_content_title">Змест з элементамі сачэння</string>
<!-- Enhanced Tracking Protection message that protection is currently on for this site -->
@ -1354,6 +1361,9 @@
<!-- Saved logins sorting strategy menu item -by last used- (if selected, it will sort saved logins by last used) -->
<string name="saved_logins_sort_strategy_last_used">Апошняе выкарыстанне</string>
<!-- Content description (not visible, for screen readers etc.): Sort saved logins dropdown menu chevron icon -->
<string name="saved_logins_menu_dropdown_chevron_icon_content_description">Меню сартавання лагінаў</string>
<!-- Title of the Add search engine screen -->
<string name="search_engine_add_custom_search_engine_title">Дадаць пашукавік</string>
<!-- Title of the Edit search engine screen -->
@ -1474,7 +1484,7 @@
<string name="saved_login_duplicate">Лагін з такім імем карыстальніка ўжо існуе</string>
<!-- Synced Tabs -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<string name="synced_tabs_connect_another_device">Падключыць іншую прыладу.</string>
<!-- Text displayed asking user to re-authenticate -->

@ -1081,7 +1081,7 @@
<!-- text for the firefox account onboarding card header when we detect you're already signed in to
another Firefox browser. (The word `Firefox` should not be translated)
The first parameter is the email of the detected user's account -->
<string name="onboarding_firefox_account_auto_signin_header_2">Xatikirisaj molojri\'ïl pa jun chik rokik\'amaya\'l Firefox achi\'el %s pa re oyonib\'äl re\'. ¿La nawajo\' natikirisaj molojri\'ïl rik\'in re rub\'i\' taqoya\'l re\'?</string>
<string name="onboarding_firefox_account_auto_signin_header_3">Xatikirisaj molojri\'ïl pa jun chik rokik\'amaya\'l Firefox achi\'el %s pa re okisab\'äl re\'. ¿La nawajo\' natikirisaj molojri\'ïl rik\'in re rub\'i\' taqoya\'l re\'?</string>
<!-- text for the button to confirm automatic sign-in -->
<string name="onboarding_firefox_account_auto_signin_confirm">Ja\', titikirisäx molojri\'ïl</string>
<!-- text for the automatic sign-in button while signing in is in process -->
@ -1333,7 +1333,7 @@
<string name="add_to_homescreen_text_placeholder">Rub\'i\' ri choj okem</string>
<!-- Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Anin yatikïr naya\' re ajk\'amaya\'l pa ri Rutikirib\'al ruxaq awoyonib\'al richin aninäq nawokisaj chuqa\' aninäq yatok pa k\'amaya\'l, achi\'el ta xa jun chokoy.</string>
<string name="add_to_homescreen_description_2">Anin yatikïr naya\' re ajk\'amaya\'l pa ri Rutikirib\'al ruxaq okisab\'al richin aninäq nawokisaj chuqa\' aninäq yatok pa k\'amaya\'l, achi\'el ta xa jun chokoy.</string>
<!-- Preference for managing the settings for logins and passwords in Fenix -->
<string name="preferences_passwords_logins_and_passwords">Kitikirisaxik molojri\'ïl chuqa\' ewan taq tzij</string>
@ -1403,8 +1403,12 @@
<string name="logins_site_copied">Xwachib\'ëx ri ruxaq pa molwuj</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a password in logins-->
<string name="saved_logins_copy_password">Tiwachib\'ëx ewan tzij</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a password while editing a login-->
<string name="saved_logins_clear_password">Tijosq\'iï ewan tzij</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a username in logins -->
<string name="saved_login_copy_username">Tiwachib\'ëx rub\'i\' winäq</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a username while editing a login -->
<string name="saved_login_clear_username">Tijosq\'ïx rub\'i\' winäq</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a site in logins -->
<string name="saved_login_copy_site">Tiwachib\'ëx ruxaq</string>
@ -1565,7 +1569,7 @@ Achi\'el: \nhttps://www.google.com/search?q=%s</string>
<string name="saved_login_duplicate">K\'o chik jun tikirib\'äl molojri\'ïl rik\'in re b\'i\'aj re\'</string>
<!-- Synced Tabs -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<string name="synced_tabs_connect_another_device">Tokisäx jun chik okisab\'äl</string>
<!-- Text displayed asking user to re-authenticate -->
<string name="synced_tabs_reauth">Tajuxub\'ej chik awi\'.</string>
@ -1585,8 +1589,6 @@ Achi\'el: \nhttps://www.google.com/search?q=%s</string>
<!-- Top Sites -->
<!-- Title text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_title">Xaq\'i\' ruchi\' ri jutaqil taq ruxaq</string>
<!-- Content description text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_content">Richin natz\'aqatisaj jun k\'ak\'a\' jutaqil ruxaq, tayuju\' jun. Tapitz\'a\' pa ruwi\' ri ruxaq richin nacha\' tiyuj.</string>
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">ÜTZ, Wetaman Chik</string>
@ -1604,4 +1606,9 @@ Achi\'el: \nhttps://www.google.com/search?q=%s</string>
<string name="no_collections_header1">Ke\'amolo\' ri taq wachinäq niqa chawa</string>
<!-- Deprecated: Label to describe what collections are to a new user without any collections -->
<string name="no_collections_description1">Ketzob\'ajïx taq kanoxïk, taq ruxaq chuqa\' taq ruwi\' ejunam richin ye\'okisäx na.</string>
<!-- Deprecated: text for the firefox account onboarding card header when we detect you're already signed in to -->
<string name="onboarding_firefox_account_auto_signin_header_2">Xatikirisaj molojri\'ïl pa jun chik rokik\'amaya\'l Firefox achi\'el %s pa re oyonib\'äl re\'. ¿La nawajo\' natikirisaj molojri\'ïl rik\'in re rub\'i\' taqoya\'l re\'?</string>
<!-- Deprecated: Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Anin yatikïr naya\' re ajk\'amaya\'l pa ri Rutikirib\'al ruxaq awoyonib\'al richin aninäq nawokisaj chuqa\' aninäq yatok pa k\'amaya\'l, achi\'el ta xa jun chokoy.</string>
</resources>

@ -517,7 +517,7 @@
<!-- Tab Management -->
<!-- Title of preference that allows a user to auto close tabs after a specified amount of time -->
<string name="preferences_close_tabs">Zavřít panely</string>
<string name="preferences_close_tabs">Zavírat panely</string>
<!-- Option for auto closing tabs that will never auto close tabs, always allows user to manually close tabs -->
<string name="close_tabs_manually">Ručně</string>
<!-- Option for auto closing tabs that will auto close tabs after one day -->
@ -1064,10 +1064,6 @@
<string name="onboarding_account_sign_in_header">S účtem Firefoxu můžete synchronizovat záložky, hesla i další svá data.</string>
<!-- Text for the button to learn more about signing in to your Firefox account -->
<string name="onboarding_manual_sign_in_learn_more">Zjistit více</string>
<!-- text for the firefox account onboarding card header when we detect you're already signed in to
another Firefox browser. (The word `Firefox` should not be translated)
The first parameter is the email of the detected user's account -->
<string name="onboarding_firefox_account_auto_signin_header_2">V dalším prohlížeči Firefox na tomto telefonu už jste přihlášení jako %s. Chcete se přihlásit tímto účtem?</string>
<!-- text for the button to confirm automatic sign-in -->
<string name="onboarding_firefox_account_auto_signin_confirm">Ano, přihlásit</string>
<!-- text for the automatic sign-in button while signing in is in process -->
@ -1309,9 +1305,6 @@
<!-- Placeholder text for the TextView in the Add to Homescreen dialog -->
<string name="add_to_homescreen_text_placeholder">Název zkratky</string>
<!-- Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Tuto stránku si můžete snadno přidat na domovskou obrazovku vašeho telefonu. Budete k ní mít okamžitý přístup a prohlížení bude rychlejší se zážitkem jako v aplikaci.</string>
<!-- Preference for managing the settings for logins and passwords in Fenix -->
<string name="preferences_passwords_logins_and_passwords">Přihlašovací údaje</string>
<!-- Preference for managing the saving of logins and passwords in Fenix -->
@ -1540,7 +1533,7 @@
<string name="saved_login_duplicate">Přihlašovací údaje s tímto uživatelským jménem už existují</string>
<!-- Synced Tabs -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<string name="synced_tabs_connect_another_device">Připojte další zařízení.</string>
<!-- Text displayed asking user to re-authenticate -->
@ -1560,8 +1553,6 @@
<!-- Top Sites -->
<!-- Title text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_title">Dosažen limit počtu top stránek</string>
<!-- Content description text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_content">Pro přidání další top stránky nejdříve nějakou odeberte. Stačí na ní podržet prst.</string>
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">OK, rozumím</string>
@ -1576,4 +1567,9 @@
<string name="no_collections_header1">Uložte si důležité věci do sbírek</string>
<!-- Deprecated: Label to describe what collections are to a new user without any collections -->
<string name="no_collections_description1">Podobná vyhledávání, stránky a panely si můžete seskupit a poté se k nim snadno vracet.</string>
<!-- Deprecated: text for the firefox account onboarding card header when we detect you're already signed in to -->
<string name="onboarding_firefox_account_auto_signin_header_2">V dalším prohlížeči Firefox na tomto telefonu už jste přihlášení jako %s. Chcete se přihlásit tímto účtem?</string>
<!-- Deprecated: Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Tuto stránku si můžete snadno přidat na domovskou obrazovku vašeho telefonu. Budete k ní mít okamžitý přístup a prohlížení bude rychlejší se zážitkem jako v aplikaci.</string>
</resources>

@ -273,6 +273,8 @@
<string name="preferences_open_links_in_a_private_tab">Links in privatem Tab öffnen</string>
<!-- Preference for allowing screenshots to be taken while in a private tab-->
<string name="preferences_allow_screenshots_in_private_mode">Bildschirmfotos im privaten Modus zulassen</string>
<!-- Will inform the user of the risk of activating Allow screenshots in private browsing option -->
<string name="preferences_screenshots_in_private_mode_disclaimer">Wenn erlaubt, sind auch private Tabs sichtbar, wenn mehrere Apps geöffnet sind</string>
<!-- Preference for adding private browsing shortcut -->
<string name="preferences_add_private_browsing_shortcut">Verknüpfung zum privaten Modus hinzufügen</string>
<!-- Preference for accessibility -->
@ -293,6 +295,8 @@
<string name="preferences_theme">Theme</string>
<!-- Preference for customizing the home screen -->
<string name="preferences_home">Startseite</string>
<!-- Preference for gestures based actions -->
<string name="preferences_gestures">Gesten</string>
<!-- Preference for settings related to visual options -->
<string name="preferences_customize">Anpassen</string>
<!-- Preference description for banner about signing in -->
@ -471,6 +475,16 @@
<!-- Preference for using following device theme -->
<string name="preference_follow_device_theme">Geräte-Theme beachten</string>
<!-- Gestures Preferences-->
<!-- Preferences for using pull to refresh in a webpage -->
<string name="preference_gestures_website_pull_to_refresh">Zum Aktualisieren ziehen</string>
<!-- Preference for using the dynamic toolbar -->
<string name="preference_gestures_dynamic_toolbar">Zum Ausblenden der Symbolleiste scrollen</string>
<!-- Preference for switching tabs by swiping horizontally on the toolbar -->
<string name="preference_gestures_swipe_toolbar_switch_tabs">Symbolleiste zur Seite wischen, um Tabs zu wechseln</string>
<!-- Preference for showing the opened tabs by swiping up on the toolbar-->
<string name="preference_gestures_swipe_toolbar_show_tabs">Nach oben wischen, ob Tabs zu öffnen</string>
<!-- Library -->
<!-- Option in Library to open Sessions page -->
<string name="library_sessions">Sitzungen</string>
@ -1086,7 +1100,7 @@
<!-- text for the firefox account onboarding card header when we detect you're already signed in to
another Firefox browser. (The word `Firefox` should not be translated)
The first parameter is the email of the detected user's account -->
<string name="onboarding_firefox_account_auto_signin_header_2">Sie sind in einem anderen Firefox-Browser auf diesem Handy als %s angemeldet. Möchten Sie sich mit diesem Konto anmelden?</string>
<string name="onboarding_firefox_account_auto_signin_header_3">Sie sind in einem anderen Firefox-Browser auf diesem Gerät als %s angemeldet. Möchten Sie sich mit diesem Konto anmelden?</string>
<!-- text for the button to confirm automatic sign-in -->
<string name="onboarding_firefox_account_auto_signin_confirm">Ja, anmelden</string>
<!-- text for the automatic sign-in button while signing in is in process -->
@ -1330,7 +1344,7 @@
<string name="add_to_homescreen_text_placeholder">Name der Verknüpfung</string>
<!-- Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Sie können diese Website einfach zum Startbildschirm Ihres Handys hinzufügen, um unmittelbaren Zugriff darauf zu haben und sie wie eine App zu nutzen.</string>
<string name="add_to_homescreen_description_2">Sie können diese Website einfach zum Startbildschirm Ihres Geräts hinzufügen, um unmittelbaren Zugriff darauf zu haben und sie wie eine App zu nutzen.</string>
<!-- Preference for managing the settings for logins and passwords in Fenix -->
<string name="preferences_passwords_logins_and_passwords">Zugangsdaten und Passwörter</string>
@ -1400,8 +1414,12 @@
<string name="logins_site_copied">Website in Zwischenablage kopiert</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a password in logins-->
<string name="saved_logins_copy_password">Passwort kopieren</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a password while editing a login-->
<string name="saved_logins_clear_password">Passwort löschen</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a username in logins -->
<string name="saved_login_copy_username">Benutzernamen kopieren</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a username while editing a login -->
<string name="saved_login_clear_username">Benutzername löschen</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a site in logins -->
<string name="saved_login_copy_site">Website kopieren</string>
<!-- Content Description (for screenreaders etc) read for the button to open a site in logins -->
@ -1557,7 +1575,7 @@
<string name="saved_login_duplicate">Es existieren bereits Zugangsdaten mit diesem Benutzernamen</string>
<!-- Synced Tabs -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<string name="synced_tabs_connect_another_device">Weiteres Gerät verbinden.</string>
<!-- Text displayed asking user to re-authenticate -->
<string name="synced_tabs_reauth">Bitte erneut authentifizieren.</string>
@ -1577,7 +1595,7 @@
<!-- Title text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_title">Obergrenze für wichtige Seiten erreicht</string>
<!-- Content description text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_content">Entfernen Sie eine der wichtigen Seiten, um eine neue hinzuzufügen. Tippen Sie lange auf die Seite und wählen Sie „Entfernen“.</string>
<string name="top_sites_max_limit_content_2">Entfernen Sie eine der wichtigen Seiten, um eine neue hinzuzufügen. Tippen und halten Sie die Seite und wählen Sie „Entfernen“.</string>
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">Ok, verstanden</string>
@ -1595,4 +1613,9 @@
<string name="no_collections_header1">Sammeln Sie die Dinge, die Ihnen wichtig sind</string>
<!-- Deprecated: Label to describe what collections are to a new user without any collections -->
<string name="no_collections_description1">Gruppieren Sie ähnliche Suchanfragen, Websites und Tabs, um später schnell darauf zugreifen zu können.</string>
</resources>
<!-- Deprecated: text for the firefox account onboarding card header when we detect you're already signed in to -->
<string name="onboarding_firefox_account_auto_signin_header_2">Sie sind in einem anderen Firefox-Browser auf diesem Handy als %s angemeldet. Möchten Sie sich mit diesem Konto anmelden?</string>
<!-- Deprecated: Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Sie können diese Website einfach zum Startbildschirm Ihres Handys hinzufügen, um unmittelbaren Zugriff darauf zu haben und sie wie eine App zu nutzen.</string>
</resources>

@ -266,6 +266,8 @@
<string name="preferences_open_links_in_a_private_tab">Open links in a private tab</string>
<!-- Preference for allowing screenshots to be taken while in a private tab-->
<string name="preferences_allow_screenshots_in_private_mode">Allow screenshots in private browsing</string>
<!-- Will inform the user of the risk of activating Allow screenshots in private browsing option -->
<string name="preferences_screenshots_in_private_mode_disclaimer">If allowed, private tabs will also be visible when multiple apps are open</string>
<!-- Preference for adding private browsing shortcut -->
<string name="preferences_add_private_browsing_shortcut">Add private browsing shortcut</string>
<!-- Preference for accessibility -->
@ -286,6 +288,8 @@
<string name="preferences_theme">Theme</string>
<!-- Preference for customizing the home screen -->
<string name="preferences_home">Home</string>
<!-- Preference for gestures based actions -->
<string name="preferences_gestures">Gestures</string>
<!-- Preference for settings related to visual options -->
<string name="preferences_customize">Customize</string>
<!-- Preference description for banner about signing in -->
@ -458,6 +462,16 @@
<!-- Preference for using following device theme -->
<string name="preference_follow_device_theme">Follow device theme</string>
<!-- Gestures Preferences-->
<!-- Preferences for using pull to refresh in a webpage -->
<string name="preference_gestures_website_pull_to_refresh">Pull to refresh</string>
<!-- Preference for using the dynamic toolbar -->
<string name="preference_gestures_dynamic_toolbar">Scroll to hide toolbar</string>
<!-- Preference for switching tabs by swiping horizontally on the toolbar -->
<string name="preference_gestures_swipe_toolbar_switch_tabs">Swipe toolbar sideways to switch tabs</string>
<!-- Preference for showing the opened tabs by swiping up on the toolbar-->
<string name="preference_gestures_swipe_toolbar_show_tabs">Swipe toolbar up to open tabs</string>
<!-- Library -->
<!-- Option in Library to open Sessions page -->
<string name="library_sessions">Sessions</string>
@ -1044,7 +1058,7 @@
<!-- text for the firefox account onboarding card header when we detect you're already signed in to
another Firefox browser. (The word `Firefox` should not be translated)
The first parameter is the email of the detected user's account -->
<string name="onboarding_firefox_account_auto_signin_header_2">You are signed in as %s on another Firefox browser on this phone. Would you like to sign in with this account?</string>
<string name="onboarding_firefox_account_auto_signin_header_3">You are signed in as %s on another Firefox browser on this device. Would you like to sign in with this account?</string>
<!-- text for the button to confirm automatic sign-in -->
<string name="onboarding_firefox_account_auto_signin_confirm">Yes, sign me in</string>
<!-- text for the automatic sign-in button while signing in is in process -->
@ -1287,7 +1301,7 @@
<string name="add_to_homescreen_text_placeholder">Shortcut name</string>
<!-- Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">You can easily add this website to your phones Home screen to have instant access and browse faster with an app-like experience.</string>
<string name="add_to_homescreen_description_2">You can easily add this website to your devices Home screen to have instant access and browse faster with an app-like experience.</string>
<!-- Preference for managing the settings for logins and passwords in Fenix -->
<string name="preferences_passwords_logins_and_passwords">Logins and passwords</string>
@ -1357,8 +1371,12 @@
<string name="logins_site_copied">Site copied to clipboard</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a password in logins-->
<string name="saved_logins_copy_password">Copy password</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a password while editing a login-->
<string name="saved_logins_clear_password">Clear password</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a username in logins -->
<string name="saved_login_copy_username">Copy username</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a username while editing a login -->
<string name="saved_login_clear_username">Clear username</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a site in logins -->
<string name="saved_login_copy_site">Copy site</string>
<!-- Content Description (for screenreaders etc) read for the button to open a site in logins -->
@ -1513,7 +1531,7 @@
<string name="saved_login_duplicate">A login with that username already exists</string>
<!-- Synced Tabs -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<string name="synced_tabs_connect_another_device">Connect another device.</string>
<!-- Text displayed asking user to re-authenticate -->
<string name="synced_tabs_reauth">Please re-authenticate.</string>
@ -1533,7 +1551,7 @@
<!-- Title text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_title">Top site limit reached</string>
<!-- Content description text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_content">To add a new top site, remove one. Long press the site and select remove.</string>
<string name="top_sites_max_limit_content_2">To add a new top site, remove one. Touch and hold the site and select remove.</string>
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">OK, Got It</string>
@ -1551,4 +1569,9 @@
<string name="no_collections_header1">Collect the things that matter to you</string>
<!-- Deprecated: Label to describe what collections are to a new user without any collections -->
<string name="no_collections_description1">Group together similar searches, sites, and tabs for quick access later.</string>
</resources>
<!-- Deprecated: text for the firefox account onboarding card header when we detect you're already signed in to -->
<string name="onboarding_firefox_account_auto_signin_header_2">You are signed in as %s on another Firefox browser on this phone. Would you like to sign in with this account?</string>
<!-- Deprecated: Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">You can easily add this website to your phones Home screen to have instant access and browse faster with an app-like experience.</string>
</resources>

@ -268,6 +268,8 @@
<string name="preferences_open_links_in_a_private_tab">Wotkazy w priwatnym rajtarku wočinić</string>
<!-- Preference for allowing screenshots to be taken while in a private tab-->
<string name="preferences_allow_screenshots_in_private_mode">Fota wobrazowki w priwatnym modusu dowolić</string>
<!-- Will inform the user of the risk of activating Allow screenshots in private browsing option -->
<string name="preferences_screenshots_in_private_mode_disclaimer">Jeli dowolene, budu priwatne rajtarki tež widźomne, hdyž wjacore nałoženja su wočinjene</string>
<!-- Preference for adding private browsing shortcut -->
<string name="preferences_add_private_browsing_shortcut">Skrótšenku za priwatny modus přidać</string>
<!-- Preference for accessibility -->
@ -288,6 +290,8 @@
<string name="preferences_theme">Drasta</string>
<!-- Preference for customizing the home screen -->
<string name="preferences_home">Startowa strona</string>
<!-- Preference for gestures based actions -->
<string name="preferences_gestures">Gesty</string>
<!-- Preference for settings related to visual options -->
<string name="preferences_customize">Přiměrić</string>
<!-- Preference description for banner about signing in -->
@ -462,6 +466,17 @@
<!-- Preference for using following device theme -->
<string name="preference_follow_device_theme">Na gratowu drastu dźiwać</string>
<!-- Gestures Preferences-->
<!-- Preferences for using pull to refresh in a webpage -->
<string name="preference_gestures_website_pull_to_refresh">Ćehńće, zo byšće aktualizował</string>
<!-- Preference for using the dynamic toolbar -->
<string name="preference_gestures_dynamic_toolbar">Kulće, zo byšće symbolowu lajstu schował</string>
<!-- Preference for switching tabs by swiping horizontally on the toolbar -->
<string name="preference_gestures_swipe_toolbar_switch_tabs">Trějće symbolowu lajstu nabok, zo byšće rajtarki přepinał</string>
<!-- Preference for showing the opened tabs by swiping up on the toolbar-->
<string name="preference_gestures_swipe_toolbar_show_tabs">Trějće symbolowu lajstu horje, zo byšće rajtarki wočinił</string>
<!-- Library -->
<!-- Option in Library to open Sessions page -->
<string name="library_sessions">Posedźenja</string>
@ -1053,7 +1068,7 @@
<!-- text for the firefox account onboarding card header when we detect you're already signed in to
another Firefox browser. (The word `Firefox` should not be translated)
The first parameter is the email of the detected user's account -->
<string name="onboarding_firefox_account_auto_signin_header_2">Sće so jako %s w druhim wobhladowaku Firefox na tutym telefonje přizjewił. Chceće so z tutym kontom přizjewić?</string>
<string name="onboarding_firefox_account_auto_signin_header_3">Sće so jako %s w druhim wobhladowaku Firefox na tutym graće přizjewił. Chceće so z tutym kontom přizjewić?</string>
<!-- text for the button to confirm automatic sign-in -->
<string name="onboarding_firefox_account_auto_signin_confirm">Haj, přizjewić</string>
<!-- text for the automatic sign-in button while signing in is in process -->
@ -1298,7 +1313,7 @@
<string name="add_to_homescreen_text_placeholder">Mjeno skrótšenki</string>
<!-- Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Móžeće startowej wobrazowce swojeho telefona tute websydło lochko přidać, zo byšće direktny přistup měł a spěšnišo z dožiwjenjom nałoženja přehladował.</string>
<string name="add_to_homescreen_description_2">Móžeće startowej wobrazowce swojeho grata tute websydło lochko přidać, zo byšće direktny přistup měł a spěšnišo z dožiwjenjom nałoženja přehladował.</string>
<!-- Preference for managing the settings for logins and passwords in Fenix -->
<string name="preferences_passwords_logins_and_passwords">Přizjewjenja a hesła</string>
@ -1368,8 +1383,12 @@
<string name="logins_site_copied">Sydło je so do mjezyskłada kopěrowało</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a password in logins-->
<string name="saved_logins_copy_password">Hesło kopěrować</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a password while editing a login-->
<string name="saved_logins_clear_password">Hesło zhašeć</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a username in logins -->
<string name="saved_login_copy_username">Wužiwarske mjeno kopěrować</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a username while editing a login -->
<string name="saved_login_clear_username">Wužiwarske mjeno zhašeć</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a site in logins -->
<string name="saved_login_copy_site">Sydło kopěrować</string>
<!-- Content Description (for screenreaders etc) read for the button to open a site in logins -->
@ -1527,7 +1546,7 @@
<string name="saved_login_duplicate">Přizjewjenje z tym wužiwarskim mjenom hižo eksistuje.</string>
<!-- Synced Tabs -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<string name="synced_tabs_connect_another_device">Z druhim gratom zwjazać.</string>
<!-- Text displayed asking user to re-authenticate -->
<string name="synced_tabs_reauth">Prošu awtentifikujće znowa.</string>
@ -1547,7 +1566,7 @@
<!-- Title text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_title">Limit za wažne sydła docpěty</string>
<!-- Content description text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_content">Zo byšće nowe wažne sydło přidał, wotstrońće jedne z nich. Tłóčće dołho na sydło a wubjerće „Wotstronić“.</string>
<string name="top_sites_max_limit_content_2">Zo byšće nowe wažne sydło přidał, wotstrońće jedne z nich. Dótkńće so sydła, dźeržće jo a wubjerće „Wotstronić“.</string>
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">W porjadku, sym zrozumił</string>
@ -1565,4 +1584,9 @@
<string name="no_collections_header1">Zběrajće wěcy, kotrež su wam wažne</string>
<!-- Deprecated: Label to describe what collections are to a new user without any collections -->
<string name="no_collections_description1">Zeskupće podobne pytanja, sydła a rajtarki za pozdźiši spěšny přistup.</string>
</resources>
<!-- Deprecated: text for the firefox account onboarding card header when we detect you're already signed in to -->
<string name="onboarding_firefox_account_auto_signin_header_2">Sće so jako %s w druhim wobhladowaku Firefox na tutym telefonje přizjewił. Chceće so z tutym kontom přizjewić?</string>
<!-- Deprecated: Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Móžeće startowej wobrazowce swojeho telefona tute websydło lochko přidać, zo byšće direktny přistup měł a spěšnišo z dožiwjenjom nałoženja přehladował.</string>
</resources>

@ -269,6 +269,8 @@
<string name="preferences_open_links_in_a_private_tab">Hivatkozások megnyitása privát lapon</string>
<!-- Preference for allowing screenshots to be taken while in a private tab-->
<string name="preferences_allow_screenshots_in_private_mode">Képernyőképek engedélyezése privát böngészésben</string>
<!-- Will inform the user of the risk of activating Allow screenshots in private browsing option -->
<string name="preferences_screenshots_in_private_mode_disclaimer">Ha engedélyezett, a privát lapok akkor is láthatóak lesznek, ha több alkalmazás van nyitva</string>
<!-- Preference for adding private browsing shortcut -->
<string name="preferences_add_private_browsing_shortcut">Privát böngészési parancsikon hozzáadása</string>
<!-- Preference for accessibility -->
@ -289,6 +291,8 @@
<string name="preferences_theme">Téma</string>
<!-- Preference for customizing the home screen -->
<string name="preferences_home">Kezdőlap</string>
<!-- Preference for gestures based actions -->
<string name="preferences_gestures">Kézmozdulatok</string>
<!-- Preference for settings related to visual options -->
<string name="preferences_customize">Testreszabás</string>
<!-- Preference description for banner about signing in -->
@ -465,6 +469,17 @@
<!-- Preference for using following device theme -->
<string name="preference_follow_device_theme">Az eszköz témájának követése</string>
<!-- Gestures Preferences-->
<!-- Preferences for using pull to refresh in a webpage -->
<string name="preference_gestures_website_pull_to_refresh">Húzza a frissítéshez</string>
<!-- Preference for using the dynamic toolbar -->
<string name="preference_gestures_dynamic_toolbar">Görgessen az eszköztár elrejtéséhez</string>
<!-- Preference for switching tabs by swiping horizontally on the toolbar -->
<string name="preference_gestures_swipe_toolbar_switch_tabs">Seperje oldalra az eszköztárat a lapok közti váltáshoz</string>
<!-- Preference for showing the opened tabs by swiping up on the toolbar-->
<string name="preference_gestures_swipe_toolbar_show_tabs">Seperje felfelé az eszköztárat a lapok megnyitásához</string>
<!-- Library -->
<!-- Option in Library to open Sessions page -->
<string name="library_sessions">Munkamenetek</string>
@ -1061,7 +1076,7 @@
<!-- text for the firefox account onboarding card header when we detect you're already signed in to
another Firefox browser. (The word `Firefox` should not be translated)
The first parameter is the email of the detected user's account -->
<string name="onboarding_firefox_account_auto_signin_header_2">A következőként van bejelentkezve egy másik Firefox böngészőben ezen a telefonon: %s. Szeretne bejelentkezni ezzel a fiókkal?</string>
<string name="onboarding_firefox_account_auto_signin_header_3">A következőként van bejelentkezve egy másik Firefox böngészőben ezen az eszközön: %s. Szeretne bejelentkezni ezzel a fiókkal?</string>
<!-- text for the button to confirm automatic sign-in -->
<string name="onboarding_firefox_account_auto_signin_confirm">Igen, jelentkeztessen be</string>
<!-- text for the automatic sign-in button while signing in is in process -->
@ -1308,7 +1323,7 @@
<string name="add_to_homescreen_text_placeholder">Parancsikon neve</string>
<!-- Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Könnyedén hozzáadhatja ezt a weboldalt a telefonja Kezdőképernyőhöz, és azonnal elérheti azt, így gyorsabban böngészve, miközben alkalmazásszerű élményt kap.</string>
<string name="add_to_homescreen_description_2">Könnyedén hozzáadhatja ezt a weboldalt az eszköze Kezdőképernyőhöz, és azonnal elérheti azt, így gyorsabban böngészve, miközben alkalmazásszerű élményt kap.</string>
<!-- Preference for managing the settings for logins and passwords in Fenix -->
<string name="preferences_passwords_logins_and_passwords">Bejelentkezések és jelszavak</string>
@ -1378,8 +1393,12 @@
<string name="logins_site_copied">Az oldal vágólapra másolva</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a password in logins-->
<string name="saved_logins_copy_password">Jelszó másolása</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a password while editing a login-->
<string name="saved_logins_clear_password">Jelszó törlése</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a username in logins -->
<string name="saved_login_copy_username">Felhasználónév másolása</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a username while editing a login -->
<string name="saved_login_clear_username">Felhasználónév törlése</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a site in logins -->
<string name="saved_login_copy_site">Oldal másolása</string>
<!-- Content Description (for screenreaders etc) read for the button to open a site in logins -->
@ -1535,7 +1554,7 @@
<string name="saved_login_duplicate">Már létezik bejelentkezés ezzel a felhasználónévvel.</string>
<!-- Synced Tabs -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<string name="synced_tabs_connect_another_device">Másik eszköz csatlakoztatása.</string>
<!-- Text displayed asking user to re-authenticate -->
<string name="synced_tabs_reauth">Hitelesítsen újra.</string>
@ -1556,7 +1575,7 @@
<!-- Title text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_title">Kedvenc oldalak korlátja elérve</string>
<!-- Content description text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_content">Új kedvenc oldal hozzáadásához távolítson el egyet. Nyomja hosszan az oldalt, és válassza az eltávolítást.</string>
<string name="top_sites_max_limit_content_2">Új kedvenc oldal hozzáadásához távolítson el egyet. Érintse meg és tartsa az ujját az oldalon, és válassza az eltávolítást.</string>
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">Rendben, értem</string>
@ -1574,4 +1593,9 @@
<string name="no_collections_header1">Gyűjtse össze az Önnek fontos dolgokat</string>
<!-- Deprecated: Label to describe what collections are to a new user without any collections -->
<string name="no_collections_description1">Csoportosítsa a hasonló kereséseket, webhelyeket és lapokat a későbbi gyors elérés érdekében.</string>
</resources>
<!-- Deprecated: text for the firefox account onboarding card header when we detect you're already signed in to -->
<string name="onboarding_firefox_account_auto_signin_header_2">A következőként van bejelentkezve egy másik Firefox böngészőben ezen a telefonon: %s. Szeretne bejelentkezni ezzel a fiókkal?</string>
<!-- Deprecated: Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Könnyedén hozzáadhatja ezt a weboldalt a telefonja Kezdőképernyőhöz, és azonnal elérheti azt, így gyorsabban böngészve, miközben alkalmazásszerű élményt kap.</string>
</resources>

@ -265,6 +265,8 @@
<string name="preferences_open_links_in_a_private_tab">פתיחת קישורים בלשונית פרטית</string>
<!-- Preference for allowing screenshots to be taken while in a private tab-->
<string name="preferences_allow_screenshots_in_private_mode">לאפשר צילומי מסך בגלישה פרטית</string>
<!-- Will inform the user of the risk of activating Allow screenshots in private browsing option -->
<string name="preferences_screenshots_in_private_mode_disclaimer">אם אפשרות זו מופעלת, לשוניות פרטיות תהיינה מוצגות כאשר מספר יישומונים פתוחים</string>
<!-- Preference for adding private browsing shortcut -->
<string name="preferences_add_private_browsing_shortcut">הוספת קיצור דרך לגלישה פרטית</string>
<!-- Preference for accessibility -->
@ -283,6 +285,8 @@
<string name="preferences_theme">ערכת נושא</string>
<!-- Preference for customizing the home screen -->
<string name="preferences_home">בית</string>
<!-- Preference for gestures based actions -->
<string name="preferences_gestures">מחוות</string>
<!-- Preference for settings related to visual options -->
<string name="preferences_customize">התאמה אישית</string>
<!-- Preference description for banner about signing in -->
@ -455,6 +459,16 @@
<!-- Preference for using following device theme -->
<string name="preference_follow_device_theme">שימוש בערכת הנושא של המכשיר</string>
<!-- Gestures Preferences-->
<!-- Preferences for using pull to refresh in a webpage -->
<string name="preference_gestures_website_pull_to_refresh">משיכה לרענון הדף</string>
<!-- Preference for using the dynamic toolbar -->
<string name="preference_gestures_dynamic_toolbar">גלילה להסתרת סרגל הכלים</string>
<!-- Preference for switching tabs by swiping horizontally on the toolbar -->
<string name="preference_gestures_swipe_toolbar_switch_tabs">החלקה על סרגל הכלים כלפי הצדדים למעבר בין לשוניות</string>
<!-- Preference for showing the opened tabs by swiping up on the toolbar-->
<string name="preference_gestures_swipe_toolbar_show_tabs">החלקה על סרגל הכלים כלפי מעלה לפתיחת לשוניות</string>
<!-- Library -->
<!-- Option in Library to open Sessions page -->
<string name="library_sessions">הפעלות</string>
@ -1046,7 +1060,7 @@
<!-- text for the firefox account onboarding card header when we detect you're already signed in to
another Firefox browser. (The word `Firefox` should not be translated)
The first parameter is the email of the detected user's account -->
<string name="onboarding_firefox_account_auto_signin_header_2">דפדפן Firefox נוסף בטלפון זה מחובר כ־%s. האם ברצונך להתחבר עם חשבון זה?</string>
<string name="onboarding_firefox_account_auto_signin_header_3">דפדפן Firefox נוסף במכשיר זה מחובר כ־%s. האם ברצונך להתחבר עם חשבון זה?</string>
<!-- text for the button to confirm automatic sign-in -->
<string name="onboarding_firefox_account_auto_signin_confirm">כן, תחברו אותי</string>
<!-- text for the automatic sign-in button while signing in is in process -->
@ -1288,7 +1302,7 @@
<string name="add_to_homescreen_text_placeholder">שם קיצור הדרך</string>
<!-- Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">באפשרותך להוסיף בקלות אתר זה למסך הבית של הטלפון שלך כדי לקבל גישה מיידית ולגלוש מהר יותר עם חוויה שמדמה שימוש ביישומון.</string>
<string name="add_to_homescreen_description_2">באפשרותך להוסיף בקלות אתר זה למסך הבית של המכשיר שלך כדי לקבל גישה מיידית ולגלוש מהר יותר עם חוויה שמדמה שימוש ביישומון.</string>
<!-- Preference for managing the settings for logins and passwords in Fenix -->
<string name="preferences_passwords_logins_and_passwords">כניסות וססמאות</string>
@ -1350,8 +1364,12 @@
<string name="logins_site_copied">האתר הועתק ללוח</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a password in logins-->
<string name="saved_logins_copy_password">העתקת ססמה</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a password while editing a login-->
<string name="saved_logins_clear_password">ניקוי ססמה</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a username in logins -->
<string name="saved_login_copy_username">העתקת שם משתמש</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a username while editing a login -->
<string name="saved_login_clear_username">ניקוי שם משתמש</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a site in logins -->
<string name="saved_login_copy_site">העתקת אתר</string>
<!-- Content Description (for screenreaders etc) read for the button to open a site in logins -->
@ -1503,7 +1521,7 @@
<string name="saved_login_duplicate">כבר קיימת כניסה עם שם משתמש זה</string>
<!-- Synced Tabs -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<string name="synced_tabs_connect_another_device">נא לחבר מכשיר נוסף.</string>
<!-- Text displayed when user has disabled tab syncing in Firefox Sync Account -->
<string name="synced_tabs_enable_tab_syncing">נא להפעיל סנכרון לשוניות.</string>
@ -1522,7 +1540,7 @@
<!-- Title text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_title">הגעת למכסת האתרים המובילים</string>
<!-- Content description text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_content">כדי להוסיף אתר מוביל חדש, יש להסיר אחד אחר. יש ללחוץ לחיצה ארוכה על האתר ולבחור ב״הסרה״.</string>
<string name="top_sites_max_limit_content_2">כדי להוסיף אתר מוביל חדש, יש להסיר אחד אחר. יש ללחוץ לחיצה ארוכה על האתר ולבחור ב״הסרה״.</string>
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">בסדר, הבנתי</string>
@ -1540,4 +1558,9 @@
<string name="no_collections_header1">לאסוף את הדברים החשובים לך</string>
<!-- Deprecated: Label to describe what collections are to a new user without any collections -->
<string name="no_collections_description1">ניתן לקבץ חיפושים, אתרים ולשוניות דומים יחד כדי לגשת אליהם מהר יותר בהמשך.</string>
</resources>
<!-- Deprecated: text for the firefox account onboarding card header when we detect you're already signed in to -->
<string name="onboarding_firefox_account_auto_signin_header_2">דפדפן Firefox נוסף בטלפון זה מחובר כ־%s. האם ברצונך להתחבר עם חשבון זה?</string>
<!-- Deprecated: Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">באפשרותך להוסיף בקלות אתר זה למסך הבית של הטלפון שלך כדי לקבל גישה מיידית ולגלוש מהר יותר עם חוויה שמדמה שימוש ביישומון.</string>
</resources>

@ -278,6 +278,8 @@
<string name="preferences_open_links_in_a_private_tab">사생활 보호 탭에 링크 열기</string>
<!-- Preference for allowing screenshots to be taken while in a private tab-->
<string name="preferences_allow_screenshots_in_private_mode">사생활 보호 모드에서 스크린샷 허용</string>
<!-- Will inform the user of the risk of activating Allow screenshots in private browsing option -->
<string name="preferences_screenshots_in_private_mode_disclaimer">허용되는 경우, 여러 앱이 열려있을 때 사생활 보호 탭도 표시됨</string>
<!-- Preference for adding private browsing shortcut -->
<string name="preferences_add_private_browsing_shortcut">사생활 보호 모드 바로 가기 추가</string>
<!-- Preference for accessibility -->
@ -298,6 +300,8 @@
<string name="preferences_theme">테마</string>
<!-- Preference for customizing the home screen -->
<string name="preferences_home"></string>
<!-- Preference for gestures based actions -->
<string name="preferences_gestures">제스처</string>
<!-- Preference for settings related to visual options -->
<string name="preferences_customize">사용자 지정</string>
<!-- Preference description for banner about signing in -->
@ -476,6 +480,16 @@
<!-- Preference for using following device theme -->
<string name="preference_follow_device_theme">기기 테마 따르기</string>
<!-- Gestures Preferences-->
<!-- Preferences for using pull to refresh in a webpage -->
<string name="preference_gestures_website_pull_to_refresh">당겨서 새로 고침</string>
<!-- Preference for using the dynamic toolbar -->
<string name="preference_gestures_dynamic_toolbar">스크롤해서 툴바 숨기기</string>
<!-- Preference for switching tabs by swiping horizontally on the toolbar -->
<string name="preference_gestures_swipe_toolbar_switch_tabs">도구 모음을 옆으로 밀어서 탭 전환</string>
<!-- Preference for showing the opened tabs by swiping up on the toolbar-->
<string name="preference_gestures_swipe_toolbar_show_tabs">도구 모음을 위로 밀어서 탭 열기</string>
<!-- Library -->
<!-- Option in Library to open Sessions page -->
<string name="library_sessions">세션</string>
@ -1097,7 +1111,7 @@
<!-- text for the firefox account onboarding card header when we detect you're already signed in to
another Firefox browser. (The word `Firefox` should not be translated)
The first parameter is the email of the detected user's account -->
<string name="onboarding_firefox_account_auto_signin_header_2">이 휴대폰의 다른 Firefox 브라우저에서 %s로 로그인했습니다. 이 계정으로 로그인하시겠습니까?</string>
<string name="onboarding_firefox_account_auto_signin_header_3">이 기기의 다른 Firefox 브라우저에서 %s(으)로 로그인했습니다. 이 계정으로 로그인하시겠습니까?</string>
<!-- text for the button to confirm automatic sign-in -->
<string name="onboarding_firefox_account_auto_signin_confirm">예, 로그인함</string>
<!-- text for the automatic sign-in button while signing in is in process -->
@ -1345,7 +1359,7 @@
<string name="add_to_homescreen_text_placeholder">바로 가기 이름</string>
<!-- Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">이 웹 사이트를 휴대폰의 홈 화면에 쉽게 추가하여 앱과 같은 경험을 통해 즉시 액세스하고 더 빠르게 탐색 할 수 있습니다.</string>
<string name="add_to_homescreen_description_2">이 웹 사이트를 기기의 홈 화면에 쉽게 추가하여 앱과 같은 경험을 통해 즉시 액세스하고 더 빠르게 탐색 할 수 있습니다.</string>
<!-- Preference for managing the settings for logins and passwords in Fenix -->
<string name="preferences_passwords_logins_and_passwords">로그인과 비밀번호</string>
@ -1415,8 +1429,12 @@
<string name="logins_site_copied">사이트가 클립보드에 복사됨</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a password in logins-->
<string name="saved_logins_copy_password">비밀번호 복사</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a password while editing a login-->
<string name="saved_logins_clear_password">비밀번호 지우기</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a username in logins -->
<string name="saved_login_copy_username">사용자 이름 복사</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a username while editing a login -->
<string name="saved_login_clear_username">사용자 이름 지우기</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a site in logins -->
<string name="saved_login_copy_site">사이트 복사</string>
<!-- Content Description (for screenreaders etc) read for the button to open a site in logins -->
@ -1574,7 +1592,7 @@
<string name="saved_login_duplicate">해당 사용자 이름을 가진 로그인이 이미 존재합니다</string>
<!-- Synced Tabs -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<string name="synced_tabs_connect_another_device">다른 기기를 연결하세요.</string>
<!-- Text displayed asking user to re-authenticate -->
<string name="synced_tabs_reauth">다시 인증하세요.</string>
@ -1595,7 +1613,7 @@
<!-- Title text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_title">상위 사이트 제한에 도달</string>
<!-- Content description text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_content">새로운 상위 사이트를 추가하려면 삭제하세요. 사이트를 길게 누르고 삭제를 선택하세요.</string>
<string name="top_sites_max_limit_content_2">새 상위 사이트를 추가하려면 하나를 삭제하세요. 사이트를 길게 터치하고 삭제를 선택하세요.</string>
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">확인</string>
@ -1613,4 +1631,9 @@
<string name="no_collections_header1">중요한 것들 수집하기</string>
<!-- Deprecated: Label to describe what collections are to a new user without any collections -->
<string name="no_collections_description1">나중에 빠르게 접근할 수 있도록 유사한 검색, 사이트 및 탭을 모아 보세요.</string>
</resources>
<!-- Deprecated: text for the firefox account onboarding card header when we detect you're already signed in to -->
<string name="onboarding_firefox_account_auto_signin_header_2">이 휴대폰의 다른 Firefox 브라우저에서 %s(으)로 로그인했습니다. 이 계정으로 로그인하시겠습니까?</string>
<!-- Deprecated: Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">이 웹 사이트를 휴대폰의 홈 화면에 쉽게 추가하여 앱과 같은 경험을 통해 즉시 액세스하고 더 빠르게 탐색 할 수 있습니다.</string>
</resources>

@ -290,6 +290,8 @@
<string name="preferences_theme">Tema</string>
<!-- Preference for customizing the home screen -->
<string name="preferences_home">Hjem</string>
<!-- Preference for gestures based actions -->
<string name="preferences_gestures">Bevegelser</string>
<!-- Preference for settings related to visual options -->
<string name="preferences_customize">Tilpass</string>
<!-- Preference description for banner about signing in -->
@ -466,6 +468,10 @@
<!-- Preference for using following device theme -->
<string name="preference_follow_device_theme">Følg enhetens tema</string>
<!-- Gestures Preferences-->
<!-- Preferences for using pull to refresh in a webpage -->
<string name="preference_gestures_website_pull_to_refresh">Trekk for å oppdatere</string>
<!-- Library -->
<!-- Option in Library to open Sessions page -->
<string name="library_sessions">Økter</string>
@ -1059,10 +1065,6 @@
<string name="onboarding_account_sign_in_header">Begynn å synkronisere bokmerker, passord og mer med Firefox-kontoen din.</string>
<!-- Text for the button to learn more about signing in to your Firefox account -->
<string name="onboarding_manual_sign_in_learn_more">Les mer</string>
<!-- text for the firefox account onboarding card header when we detect you're already signed in to
another Firefox browser. (The word `Firefox` should not be translated)
The first parameter is the email of the detected user's account -->
<string name="onboarding_firefox_account_auto_signin_header_2">Du er logget inn som %s på en annen Firefox-nettleser på denne telefonen. Vil du logge inn med denne kontoen?</string>
<!-- text for the button to confirm automatic sign-in -->
<string name="onboarding_firefox_account_auto_signin_confirm">Ja, logg meg inn</string>
<!-- text for the automatic sign-in button while signing in is in process -->
@ -1313,9 +1315,6 @@
<!-- Placeholder text for the TextView in the Add to Homescreen dialog -->
<string name="add_to_homescreen_text_placeholder">Navn på snarvei</string>
<!-- Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Du kan enkelt legge til dette nettstedet på telefonens startskjermen for å få øyeblikkelig tilgang og surfe raskere med en app-lignende opplevelse.</string>
<!-- Preference for managing the settings for logins and passwords in Fenix -->
<string name="preferences_passwords_logins_and_passwords">Innlogginger og passord</string>
<!-- Preference for managing the saving of logins and passwords in Fenix -->
@ -1547,7 +1546,7 @@
<string name="saved_login_duplicate">En innlogging med det brukernavnet eksisterer allerede</string>
<!-- Synced Tabs -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<string name="synced_tabs_connect_another_device">Koble til en annen enhet.</string>
<!-- Text displayed asking user to re-authenticate -->
<string name="synced_tabs_reauth">Autentiser på nytt.</string>
@ -1566,8 +1565,6 @@
<!-- Top Sites -->
<!-- Title text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_title">Grense for populære nettsteder nådd</string>
<!-- Content description text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_content">For å legge til et nytt populært nettsted må du fjern en annen. Trykk lenge på nettstedet og velg fjern.</string>
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">OK, jeg skjønner</string>
@ -1585,4 +1582,9 @@
<string name="no_collections_header1">Samle tingene som betyr noe for deg</string>
<!-- Deprecated: Label to describe what collections are to a new user without any collections -->
<string name="no_collections_description1">Grupper sammen lignende søk, nettsteder og faner for rask tilgang senere.</string>
</resources>
<!-- Deprecated: text for the firefox account onboarding card header when we detect you're already signed in to -->
<string name="onboarding_firefox_account_auto_signin_header_2">Du er logget inn som %s på en annen Firefox-nettleser på denne telefonen. Vil du logge inn med denne kontoen?</string>
<!-- Deprecated: Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Du kan enkelt legge til dette nettstedet på telefonens startskjermen for å få øyeblikkelig tilgang og surfe raskere med en app-lignende opplevelse.</string>
</resources>

@ -268,6 +268,8 @@
<string name="preferences_open_links_in_a_private_tab">Abrir links em abas privativas</string>
<!-- Preference for allowing screenshots to be taken while in a private tab-->
<string name="preferences_allow_screenshots_in_private_mode">Permitir capturas de tela na navegação privativa</string>
<!-- Will inform the user of the risk of activating Allow screenshots in private browsing option -->
<string name="preferences_screenshots_in_private_mode_disclaimer">Se permitido, as abas privativas também ficam visíveis quando vários aplicativos estão abertos</string>
<!-- Preference for adding private browsing shortcut -->
<string name="preferences_add_private_browsing_shortcut">Adicionar atalho para navegação privativa</string>
<!-- Preference for accessibility -->
@ -288,6 +290,8 @@
<string name="preferences_theme">Tema</string>
<!-- Preference for customizing the home screen -->
<string name="preferences_home">Tela inicial</string>
<!-- Preference for gestures based actions -->
<string name="preferences_gestures">Gestos</string>
<!-- Preference for settings related to visual options -->
<string name="preferences_customize">Personalizar</string>
<!-- Preference description for banner about signing in -->
@ -462,6 +466,16 @@
<!-- Preference for using following device theme -->
<string name="preference_follow_device_theme">Acompanhar tema do dispositivo</string>
<!-- Gestures Preferences-->
<!-- Preferences for using pull to refresh in a webpage -->
<string name="preference_gestures_website_pull_to_refresh">Puxe para atualizar</string>
<!-- Preference for using the dynamic toolbar -->
<string name="preference_gestures_dynamic_toolbar">Deslize para ocultar a barra de ferramentas</string>
<!-- Preference for switching tabs by swiping horizontally on the toolbar -->
<string name="preference_gestures_swipe_toolbar_switch_tabs">Deslize a barra de ferramentas para o lado para mudar de aba</string>
<!-- Preference for showing the opened tabs by swiping up on the toolbar-->
<string name="preference_gestures_swipe_toolbar_show_tabs">Deslize a barra de ferramentas para cima para abrir abas</string>
<!-- Library -->
<!-- Option in Library to open Sessions page -->
<string name="library_sessions">Sessões</string>
@ -1052,7 +1066,7 @@
<!-- text for the firefox account onboarding card header when we detect you're already signed in to
another Firefox browser. (The word `Firefox` should not be translated)
The first parameter is the email of the detected user's account -->
<string name="onboarding_firefox_account_auto_signin_header_2">Você está conectado como %s em outro navegador Firefox neste celular. Quer entrar com esta conta?</string>
<string name="onboarding_firefox_account_auto_signin_header_3">Você está conectado como %s em outro navegador Firefox neste dispositivo. Quer entrar com esta conta?</string>
<!-- text for the button to confirm automatic sign-in -->
<string name="onboarding_firefox_account_auto_signin_confirm">Sim, entrar</string>
<!-- text for the automatic sign-in button while signing in is in process -->
@ -1299,7 +1313,7 @@
<string name="add_to_homescreen_text_placeholder">Nome do atalho</string>
<!-- Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Você pode facilmente adicionar este site à tela inicial do celular para ter acesso imediato e navegar mais rápido com uma experiência semelhante a um aplicativo.</string>
<string name="add_to_homescreen_description_2">Você pode facilmente adicionar este site à tela inicial do dispositivo para ter acesso imediato e navegar mais rápido com uma experiência semelhante a um aplicativo.</string>
<!-- Preference for managing the settings for logins and passwords in Fenix -->
<string name="preferences_passwords_logins_and_passwords">Contas e senhas</string>
@ -1369,8 +1383,12 @@
<string name="logins_site_copied">Site copiado para a área de transferência</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a password in logins-->
<string name="saved_logins_copy_password">Copiar senha</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a password while editing a login-->
<string name="saved_logins_clear_password">Limpar senha</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a username in logins -->
<string name="saved_login_copy_username">Copiar nome de usuário</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a username while editing a login -->
<string name="saved_login_clear_username">Limpar nome de usuário</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a site in logins -->
<string name="saved_login_copy_site">Copiar site</string>
<!-- Content Description (for screenreaders etc) read for the button to open a site in logins -->
@ -1526,7 +1544,7 @@
<string name="saved_login_duplicate">Já existe uma conta com este nome de usuário</string>
<!-- Synced Tabs -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<string name="synced_tabs_connect_another_device">Conecte outro dispositivo.</string>
<!-- Text displayed asking user to re-authenticate -->
<string name="synced_tabs_reauth">Autentique novamente.</string>
@ -1547,7 +1565,7 @@
<!-- Title text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_title">Atingiu o limite de sites preferidos</string>
<!-- Content description text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_content">Para adicionar mais um site preferido, remova outro. Mantenha o dedo sobre o site e selecione remover.</string>
<string name="top_sites_max_limit_content_2">Para adicionar mais um site preferido, remova outro. Mantenha o dedo sobre o site e selecione remover.</string>
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">OK, entendi</string>
@ -1565,4 +1583,9 @@
<string name="no_collections_header1">Reúna o que é importante para você</string>
<!-- Deprecated: Label to describe what collections are to a new user without any collections -->
<string name="no_collections_description1">Agrupe pesquisas, sites e abas semelhantes para acesso rápido mais tarde.</string>
</resources>
<!-- Deprecated: text for the firefox account onboarding card header when we detect you're already signed in to -->
<string name="onboarding_firefox_account_auto_signin_header_2">Você está conectado como %s em outro navegador Firefox neste celular. Quer entrar com esta conta?</string>
<!-- Deprecated: Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Você pode facilmente adicionar este site à tela inicial do celular para ter acesso imediato e navegar mais rápido com uma experiência semelhante a um aplicativo.</string>
</resources>

@ -272,6 +272,8 @@
<string name="preferences_open_links_in_a_private_tab">Öppna länkar i en privat flik</string>
<!-- Preference for allowing screenshots to be taken while in a private tab-->
<string name="preferences_allow_screenshots_in_private_mode">Tillåt skärmdumpar i privat surfning</string>
<!-- Will inform the user of the risk of activating Allow screenshots in private browsing option -->
<string name="preferences_screenshots_in_private_mode_disclaimer">Om tillåtet kommer privata flikar också att visas när flera appar är öppna</string>
<!-- Preference for adding private browsing shortcut -->
<string name="preferences_add_private_browsing_shortcut">Lägg till genväg för privat surfning</string>
<!-- Preference for accessibility -->
@ -292,6 +294,8 @@
<string name="preferences_theme">Tema</string>
<!-- Preference for customizing the home screen -->
<string name="preferences_home">Hem</string>
<!-- Preference for gestures based actions -->
<string name="preferences_gestures">Gester</string>
<!-- Preference for settings related to visual options -->
<string name="preferences_customize">Anpassa</string>
<!-- Preference description for banner about signing in -->
@ -470,6 +474,16 @@
<!-- Preference for using following device theme -->
<string name="preference_follow_device_theme">Följ enhetens tema</string>
<!-- Gestures Preferences-->
<!-- Preferences for using pull to refresh in a webpage -->
<string name="preference_gestures_website_pull_to_refresh">Dra för att uppdatera</string>
<!-- Preference for using the dynamic toolbar -->
<string name="preference_gestures_dynamic_toolbar">Bläddra för att dölja verktygsfältet</string>
<!-- Preference for switching tabs by swiping horizontally on the toolbar -->
<string name="preference_gestures_swipe_toolbar_switch_tabs">Svep verktygsfältet i sidled för att byta flik</string>
<!-- Preference for showing the opened tabs by swiping up on the toolbar-->
<string name="preference_gestures_swipe_toolbar_show_tabs">Svep verktygsfältet uppåt för att öppna flikar</string>
<!-- Library -->
<!-- Option in Library to open Sessions page -->
<string name="library_sessions">Sessioner</string>
@ -1068,7 +1082,7 @@
<!-- text for the firefox account onboarding card header when we detect you're already signed in to
another Firefox browser. (The word `Firefox` should not be translated)
The first parameter is the email of the detected user's account -->
<string name="onboarding_firefox_account_auto_signin_header_2">Du är inloggad som %s på en annan Firefox-webbläsare på den här telefonen. Vill du logga in med det här kontot?</string>
<string name="onboarding_firefox_account_auto_signin_header_3">Du är inloggad som %s i en annan Firefox-webbläsare på den här enheten. Vill du logga in med det här kontot?</string>
<!-- text for the button to confirm automatic sign-in -->
<string name="onboarding_firefox_account_auto_signin_confirm">Ja, logga in mig</string>
<!-- text for the automatic sign-in button while signing in is in process -->
@ -1315,7 +1329,7 @@
<string name="add_to_homescreen_text_placeholder">Genvägens namn</string>
<!-- Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Du kan enkelt lägga till den här webbplatsen på telefonens startsida för att få direktåtkomst och surfa snabbare med en appliknande upplevelse.</string>
<string name="add_to_homescreen_description_2">Du kan enkelt lägga till den här webbplatsen på enhetens startskärm för att få direktåtkomst och surfa snabbare med en appliknande upplevelse.</string>
<!-- Preference for managing the settings for logins and passwords in Fenix -->
<string name="preferences_passwords_logins_and_passwords">Inloggningar och lösenord</string>
@ -1385,8 +1399,12 @@
<string name="logins_site_copied">Webbplats kopierad till urklipp</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a password in logins-->
<string name="saved_logins_copy_password">Kopiera lösenord</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a password while editing a login-->
<string name="saved_logins_clear_password">Rensa lösenord</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a username in logins -->
<string name="saved_login_copy_username">Kopiera användarnamn</string>
<!-- Content Description (for screenreaders etc) read for the button to clear a username while editing a login -->
<string name="saved_login_clear_username">Rensa användarnamn</string>
<!-- Content Description (for screenreaders etc) read for the button to copy a site in logins -->
<string name="saved_login_copy_site">Kopiera webbplats</string>
<!-- Content Description (for screenreaders etc) read for the button to open a site in logins -->
@ -1542,7 +1560,7 @@
<string name="saved_login_duplicate">En inloggning med det användarnamnet finns redan</string>
<!-- Synced Tabs -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<string name="synced_tabs_connect_another_device">Anslut en annan enhet.</string>
<!-- Text displayed asking user to re-authenticate -->
<string name="synced_tabs_reauth">Bekräfta igen.</string>
@ -1562,7 +1580,7 @@
<!-- Title text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_title">Övre gräns för mest besökta nådd</string>
<!-- Content description text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_content">För att lägga till en ny mest besökt sida, ta bort en. Tryck länge på webbplatsen och välj ta bort.</string>
<string name="top_sites_max_limit_content_2">För att lägga till en ny mest besökt sida, ta bort en. Tryck länge på webbplatsen och välj ta bort.</string>
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">Ok, jag förstår</string>
@ -1580,4 +1598,9 @@
<string name="no_collections_header1">Samla de saker som är viktiga för dig</string>
<!-- Deprecated: Label to describe what collections are to a new user without any collections -->
<string name="no_collections_description1">Gruppera liknande sökningar, webbplatser och flikar för snabb åtkomst senare.</string>
</resources>
<!-- Deprecated: text for the firefox account onboarding card header when we detect you're already signed in to -->
<string name="onboarding_firefox_account_auto_signin_header_2">Du är inloggad som %s på en annan Firefox-webbläsare på den här telefonen. Vill du logga in med det här kontot?</string>
<!-- Deprecated: Describes the add to homescreen functionality -->
<string name="add_to_homescreen_description">Du kan enkelt lägga till den här webbplatsen på telefonens startsida för att få direktåtkomst och surfa snabbare med en appliknande upplevelse.</string>
</resources>

@ -22,6 +22,7 @@
<dimen name="preference_icon_drawable_size">24dp</dimen>
<dimen name="search_bar_search_engine_icon_padding">12dp</dimen>
<dimen name="search_bar_search_icon_margin">28dp</dimen>
<dimen name="search_engine_engine_icon_margin">12dp</dimen>
<dimen name="search_engine_radio_button_height">48dp</dimen>
<dimen name="search_engine_engine_icon_height">24dp</dimen>
<dimen name="radio_button_drawable_padding">32dp</dimen>
@ -176,7 +177,8 @@
<dimen name="top_sites_favicon_size">40dp</dimen>
<dimen name="top_sites_favicon_padding">4dp</dimen>
<dimen name="top_sites_item_size">64dp</dimen>
<dimen name="top_sites_item_margin_top">12dp</dimen>
<dimen name="top_sites_item_margin_top">8dp</dimen>
<dimen name="top_sites_item_margin_bottom">12dp</dimen>
<dimen name="top_sites_text_margin_top">8dp</dimen>
<!-- a11y -->

@ -97,6 +97,7 @@
<string name="pref_key_show_search_suggestions_in_private" translatable="false">pref_key_show_search_suggestions_in_private</string>
<string name="pref_key_show_search_suggestions_in_private_onboarding" translatable="false">pref_key_show_search_suggestions_in_privateonboarding</string>
<string name="pref_key_show_voice_search" translatable="false">pref_key_show_voice_search</string>
<string name="pref_key_enable_autocomplete_urls" translatable="false">pref_key_enable_domain_autocomplete</string>
<!-- Site Permissions Settings -->
<string name="pref_key_optimize" translatable="false">pref_key_optimize</string>
@ -241,4 +242,6 @@
<string name="pref_key_close_tabs_after_one_day" translatable="false">pref_key_close_tabs_after_one_day</string>
<string name="pref_key_close_tabs_after_one_week" translatable="false">pref_key_close_tabs_after_one_week</string>
<string name="pref_key_close_tabs_after_one_month" translatable="false">pref_key_close_tabs_after_one_month</string>
<string name="pref_key_camera_permissions" translatable="false">pref_key_camera_permissions</string>
</resources>

@ -328,6 +328,8 @@
<string name="preferences_search_bookmarks">Search bookmarks</string>
<!-- Preference for account settings -->
<string name="preferences_account_settings">Account settings</string>
<!-- Preference for enabling url autocomplete-->
<string name="preferences_enable_autocomplete_urls">Autocomplete URLs</string>
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Open links in apps</string>
<!-- Preference for open download with an external download manager app -->
@ -1591,7 +1593,7 @@
<!-- Content description for close button in collection placeholder. -->
<string name="remove_home_collection_placeholder_content_description">Remove</string>
<!-- depcrecated: text for the firefox account onboarding card header
<!-- Deprecated: text for the firefox account onboarding card header
The first parameter is the name of the app (e.g. Firefox Preview) -->
<string name="onboarding_firefox_account_header">Get the most out of %s.</string>

@ -28,6 +28,10 @@
android:defaultValue="true"
android:key="@string/pref_key_show_search_engine_shortcuts"
android:title="@string/preferences_show_search_engines" />
<SwitchPreference
android:defaultValue="true"
android:key="@string/pref_key_enable_autocomplete_urls"
android:title="@string/preferences_enable_autocomplete_urls" />
<SwitchPreference
android:defaultValue="true"
android:key="@string/pref_key_show_clipboard_suggestions"

@ -10,12 +10,12 @@ import io.mockk.mockk
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
import mozilla.components.concept.base.crash.CrashReporting
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.Settings
import mozilla.components.concept.fetch.Client
import mozilla.components.feature.pwa.WebAppShortcutManager
import mozilla.components.feature.top.sites.DefaultTopSitesStorage
import mozilla.components.support.base.crash.CrashReporting
class TestCore(context: Context, crashReporter: CrashReporting) : Core(context, crashReporter) {

@ -12,10 +12,10 @@ import io.mockk.Called
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import mozilla.components.concept.base.crash.Breadcrumb
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

@ -37,6 +37,7 @@ import org.mozilla.fenix.browser.readermode.ReaderModeController
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.settings
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
@ -241,12 +242,23 @@ class DefaultBrowserToolbarControllerTest {
}
@Test
fun handleScroll() {
fun `handleScroll for dynamic toolbars`() {
val controller = createController()
every { activity.settings().isDynamicToolbarEnabled } returns true
controller.handleScroll(10)
verify { engineView.setVerticalClipping(10) }
}
@Test
fun `handleScroll for static toolbars`() {
val controller = createController()
every { activity.settings().isDynamicToolbarEnabled } returns false
controller.handleScroll(10)
verify(exactly = 0) { engineView.setVerticalClipping(10) }
}
private fun createController(
activity: HomeActivity = this.activity,
customTabSession: Session? = null,

@ -2,6 +2,7 @@ package org.mozilla.fenix.ext
import android.content.Intent
import androidx.navigation.NavDirections
import androidx.navigation.NavOptions
import io.mockk.Matcher
import io.mockk.MockKMatcherScope
import io.mockk.internalSubstitute
@ -12,6 +13,11 @@ import mozilla.components.support.ktx.android.os.contentEquals
*/
fun MockKMatcherScope.directionsEq(value: NavDirections) = match(EqNavDirectionsMatcher(value))
/**
* Verify that an equal [NavOptions] object was passed in a MockK verify call.
*/
fun MockKMatcherScope.optionsEq(value: NavOptions) = match(EqNavOptionsMatcher(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.
@ -28,6 +34,15 @@ private data class EqNavDirectionsMatcher(private val value: NavDirections) : Ma
copy(value = value.internalSubstitute(map))
}
private data class EqNavOptionsMatcher(private val value: NavOptions) : Matcher<NavOptions> {
override fun match(arg: NavOptions?): Boolean =
value.popUpTo == arg?.popUpTo && value.isPopUpToInclusive == arg.isPopUpToInclusive
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)

@ -0,0 +1,171 @@
/* 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.library.recentlyclosed
import android.content.ClipData
import android.content.ClipboardManager
import android.content.res.Resources
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.state.ClosedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.recentlyclosed.ext.restoreTab
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.directionsEq
import org.mozilla.fenix.ext.optionsEq
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
// Robolectric needed for `onShareItem()`
@ExperimentalCoroutinesApi
@RunWith(FenixRobolectricTestRunner::class)
class DefaultRecentlyClosedControllerTest {
private val dispatcher = TestCoroutineDispatcher()
private val navController: NavController = mockk(relaxed = true)
private val resources: Resources = mockk(relaxed = true)
private val snackbar: FenixSnackbar = mockk(relaxed = true)
private val clipboardManager: ClipboardManager = mockk(relaxed = true)
private val openToBrowser: (ClosedTab, BrowsingMode?) -> Unit = mockk(relaxed = true)
private val sessionManager: SessionManager = mockk(relaxed = true)
private val activity: HomeActivity = mockk(relaxed = true)
private val store: BrowserStore = mockk(relaxed = true)
val mockedTab: ClosedTab = mockk(relaxed = true)
private val controller = DefaultRecentlyClosedController(
navController,
store,
sessionManager,
resources,
snackbar,
clipboardManager,
activity,
openToBrowser
)
@Before
fun setUp() {
mockkStatic("mozilla.components.feature.recentlyclosed.ext.ClosedTabKt")
every { mockedTab.restoreTab(any(), any(), any()) } just Runs
}
@After
fun tearDown() {
dispatcher.cleanupTestCoroutines()
unmockkStatic("mozilla.components.feature.recentlyclosed.ext.ClosedTabKt")
}
@Test
fun handleOpen() {
val item: ClosedTab = mockk(relaxed = true)
controller.handleOpen(item, BrowsingMode.Private)
verify {
openToBrowser(item, BrowsingMode.Private)
}
controller.handleOpen(item, BrowsingMode.Normal)
verify {
openToBrowser(item, BrowsingMode.Normal)
}
}
@Test
fun handleDeleteOne() {
val item: ClosedTab = mockk(relaxed = true)
controller.handleDeleteOne(item)
verify {
store.dispatch(RecentlyClosedAction.RemoveClosedTabAction(item))
}
}
@Test
fun handleNavigateToHistory() {
controller.handleNavigateToHistory()
verify {
navController.navigate(
directionsEq(
RecentlyClosedFragmentDirections.actionGlobalHistoryFragment()
),
optionsEq(NavOptions.Builder().setPopUpTo(R.id.historyFragment, true).build())
)
}
}
@Test
fun handleCopyUrl() {
val item = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
val clipdata = slot<ClipData>()
controller.handleCopyUrl(item)
verify {
clipboardManager.setPrimaryClip(capture(clipdata))
snackbar.show()
}
assertEquals(1, clipdata.captured.itemCount)
assertEquals("mozilla.org", clipdata.captured.description.label)
assertEquals("mozilla.org", clipdata.captured.getItemAt(0).text)
}
@Test
@Suppress("UNCHECKED_CAST")
fun handleShare() {
val item = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
controller.handleShare(item)
verify {
navController.navigate(
directionsEq(
RecentlyClosedFragmentDirections.actionGlobalShareFragment(
data = arrayOf(ShareData(url = item.url, title = item.title))
)
)
)
}
}
@Test
fun handleRestore() {
controller.handleRestore(mockedTab)
dispatcher.advanceUntilIdle()
verify {
mockedTab.restoreTab(
store,
sessionManager,
onTabRestored = any()
)
}
}
}

@ -0,0 +1,96 @@
/* 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.library.recentlyclosed
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.browser.state.state.ClosedTab
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
class RecentlyClosedFragmentInteractorTest {
lateinit var interactor: RecentlyClosedFragmentInteractor
private val defaultRecentlyClosedController: DefaultRecentlyClosedController =
mockk(relaxed = true)
@Before
fun setup() {
interactor =
RecentlyClosedFragmentInteractor(
recentlyClosedController = defaultRecentlyClosedController
)
}
@Test
fun open() {
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
interactor.restore(tab)
verify {
defaultRecentlyClosedController.handleRestore(tab)
}
}
@Test
fun onCopyPressed() {
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
interactor.onCopyPressed(tab)
verify {
defaultRecentlyClosedController.handleCopyUrl(tab)
}
}
@Test
fun onSharePressed() {
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
interactor.onSharePressed(tab)
verify {
defaultRecentlyClosedController.handleShare(tab)
}
}
@Test
fun onOpenInNormalTab() {
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
interactor.onOpenInNormalTab(tab)
verify {
defaultRecentlyClosedController.handleOpen(tab, mode = BrowsingMode.Normal)
}
}
@Test
fun onOpenInPrivateTab() {
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
interactor.onOpenInPrivateTab(tab)
verify {
defaultRecentlyClosedController.handleOpen(tab, mode = BrowsingMode.Private)
}
}
@Test
fun onDeleteOne() {
val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
interactor.onDeleteOne(tab)
verify {
defaultRecentlyClosedController.handleDeleteOne(tab)
}
}
@Test
fun onNavigateToHistory() {
interactor.onNavigateToHistory()
verify {
defaultRecentlyClosedController.handleNavigateToHistory()
}
}
}

@ -4,6 +4,7 @@
package org.mozilla.fenix.search
import androidx.appcompat.app.AlertDialog
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.MockKAnnotations
@ -13,6 +14,7 @@ import io.mockk.impl.annotations.MockK
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.spyk
import io.mockk.unmockkObject
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -32,6 +34,8 @@ import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Settings
typealias AlertDialogBuilder = AlertDialog.Builder
@ExperimentalCoroutinesApi
class DefaultSearchControllerTest {
@ -58,7 +62,6 @@ class DefaultSearchControllerTest {
every { id } returns R.id.searchFragment
}
every { MetricsUtils.createSearchEvent(searchEngine, activity, any()) } returns null
controller = DefaultSearchController(
activity = activity,
sessionManager = sessionManager,
@ -328,4 +331,16 @@ class DefaultSearchControllerTest {
verify { sessionManager.select(any()) }
verify { activity.openToBrowser(from = BrowserDirection.FromSearch) }
}
@Test
fun `show camera permissions needed dialog`() {
val dialogBuilder: AlertDialogBuilder = mockk(relaxed = true)
val spyController = spyk(controller)
every { spyController.buildDialog() } returns dialogBuilder
spyController.handleCameraPermissionsNeeded()
verify { dialogBuilder.show() }
}
}

@ -13,6 +13,7 @@ import io.mockk.impl.annotations.MockK
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.spyk
import io.mockk.unmockkObject
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -29,6 +30,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.search.AlertDialogBuilder
import org.mozilla.fenix.search.SearchFragmentAction
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Settings
@ -44,6 +46,7 @@ class SearchDialogControllerTest {
@MockK(relaxed = true) private lateinit var settings: Settings
@MockK private lateinit var sessionManager: SessionManager
@MockK(relaxed = true) private lateinit var clearToolbarFocus: () -> Unit
@MockK(relaxed = true) private lateinit var dismissDialog: () -> Unit
private lateinit var controller: SearchDialogController
@ -67,6 +70,7 @@ class SearchDialogControllerTest {
navController = navController,
settings = settings,
metrics = metrics,
dismissDialog = dismissDialog,
clearToolbarFocus = clearToolbarFocus
)
}
@ -93,6 +97,17 @@ class SearchDialogControllerTest {
verify { metrics.track(Event.EnteredUrl(false)) }
}
@Test
fun handleBlankUrlCommitted() {
val url = ""
controller.handleUrlCommitted(url)
verify {
dismissDialog()
}
}
@Test
fun handleSearchCommitted() {
val searchTerm = "Firefox"
@ -329,4 +344,16 @@ class SearchDialogControllerTest {
verify { sessionManager.select(any()) }
verify { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) }
}
@Test
fun `show camera permissions needed dialog`() {
val dialogBuilder: AlertDialogBuilder = mockk(relaxed = true)
val spyController = spyk(controller)
every { spyController.buildDialog() } returns dialogBuilder
spyController.handleCameraPermissionsNeeded()
verify { dialogBuilder.show() }
}
}

@ -13,6 +13,8 @@ import kotlinx.coroutines.GlobalScope.coroutineContext
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.tabs.TabsUseCases
@ -31,6 +33,7 @@ class DefaultDeleteBrowsingDataControllerTest {
private var removeAllTabs: TabsUseCases.RemoveAllTabsUseCase = mockk(relaxed = true)
private var historyStorage: HistoryStorage = mockk(relaxed = true)
private var permissionStorage: PermissionStorage = mockk(relaxed = true)
private var store: BrowserStore = mockk(relaxed = true)
private var iconsStorage: BrowserIcons = mockk(relaxed = true)
private val engine: Engine = mockk(relaxed = true)
private lateinit var controller: DefaultDeleteBrowsingDataController
@ -40,6 +43,7 @@ class DefaultDeleteBrowsingDataControllerTest {
controller = DefaultDeleteBrowsingDataController(
removeAllTabs = removeAllTabs,
historyStorage = historyStorage,
store = store,
permissionStorage = permissionStorage,
iconsStorage = iconsStorage,
engine = engine,
@ -65,6 +69,7 @@ class DefaultDeleteBrowsingDataControllerTest {
coVerify {
engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES))
historyStorage.deleteEverything()
store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction)
iconsStorage.clear()
}
}

@ -18,7 +18,7 @@ import io.mockk.verifyOrder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.engine.profiler.Profiler
import mozilla.components.concept.base.profiler.Profiler
import mozilla.components.concept.tabstray.Tab
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.TabsUseCases

@ -1,89 +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.wifi
import io.mockk.Called
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_AUDIBLE
import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_INAUDIBLE
import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_ALLOW_ALL
import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_ALLOW_ON_WIFI
import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_ALL
import org.mozilla.fenix.utils.Settings
class SitePermissionsWifiIntegrationTest {
private lateinit var settings: Settings
private lateinit var wifiConnectionMonitor: WifiConnectionMonitor
private lateinit var wifiIntegration: SitePermissionsWifiIntegration
@Before
fun setup() {
settings = mockk()
wifiConnectionMonitor = mockk(relaxed = true)
wifiIntegration = SitePermissionsWifiIntegration(settings, wifiConnectionMonitor)
every { settings.setSitePermissionsPhoneFeatureAction(any(), any()) } just Runs
}
@Test
fun `add and remove wifi connected listener`() {
wifiIntegration.addWifiConnectedListener()
verify { wifiConnectionMonitor.register(any()) }
wifiIntegration.removeWifiConnectedListener()
verify { wifiConnectionMonitor.unregister(any()) }
}
@Test
fun `start and stop wifi connection monitor`() {
wifiIntegration.start()
verify { wifiConnectionMonitor.start() }
wifiIntegration.stop()
verify { wifiConnectionMonitor.stop() }
}
@Test
fun `add only if autoplay is only allowed on wifi`() {
every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ALL
wifiIntegration.maybeAddWifiConnectedListener()
verify { wifiConnectionMonitor wasNot Called }
every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ON_WIFI
wifiIntegration.maybeAddWifiConnectedListener()
verify { wifiConnectionMonitor.register(any()) }
}
@Test
fun `listener removes itself if autoplay is not only allowed on wifi`() {
every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ALL
wifiIntegration.onWifiConnectionChanged(connected = true)
verify { wifiConnectionMonitor.unregister(any()) }
}
@Test
fun `listener sets audible and inaudible settings to allowed on connect`() {
every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ON_WIFI
wifiIntegration.onWifiConnectionChanged(connected = true)
verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_AUDIBLE, Action.ALLOWED) }
verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_INAUDIBLE, Action.ALLOWED) }
}
@Test
fun `listener sets audible and inaudible settings to blocked on disconnected`() {
every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ON_WIFI
wifiIntegration.onWifiConnectionChanged(connected = false)
verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_AUDIBLE, Action.BLOCKED) }
verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_INAUDIBLE, Action.BLOCKED) }
}
}

@ -1,91 +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.wifi
import android.net.ConnectivityManager
import android.net.NetworkRequest
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkConstructor
import io.mockk.slot
import io.mockk.unmockkConstructor
import io.mockk.verify
import org.junit.After
import org.junit.Before
import org.junit.Test
class WifiConnectionMonitorTest {
private lateinit var connectivityManager: ConnectivityManager
private lateinit var wifiConnectionMonitor: WifiConnectionMonitor
@Before
fun setup() {
mockkConstructor(NetworkRequest.Builder::class)
connectivityManager = mockk(relaxUnitFun = true)
wifiConnectionMonitor = WifiConnectionMonitor(connectivityManager)
every {
anyConstructed<NetworkRequest.Builder>().addTransportType(any())
} answers { self as NetworkRequest.Builder }
}
@After
fun teardown() {
unmockkConstructor(NetworkRequest.Builder::class)
}
@Test
fun `start runs only once`() {
wifiConnectionMonitor.start()
wifiConnectionMonitor.start()
verify(exactly = 1) {
connectivityManager.registerNetworkCallback(any(), any<ConnectivityManager.NetworkCallback>())
}
}
@Test
fun `stop only runs after start`() {
wifiConnectionMonitor.stop()
verify(exactly = 0) {
connectivityManager.unregisterNetworkCallback(any<ConnectivityManager.NetworkCallback>())
}
wifiConnectionMonitor.start()
wifiConnectionMonitor.stop()
verify {
connectivityManager.unregisterNetworkCallback(any<ConnectivityManager.NetworkCallback>())
}
}
@Test
fun `passes results from connectivity manager to observers`() {
val slot = slot<ConnectivityManager.NetworkCallback>()
every { connectivityManager.registerNetworkCallback(any(), capture(slot)) } just Runs
wifiConnectionMonitor.start()
// Immediately notifies observer when registered
val observer = mockk<WifiConnectionMonitor.Observer>(relaxed = true)
wifiConnectionMonitor.register(observer)
verify { observer.onWifiConnectionChanged(connected = false) }
// Notifies observer when network is available or lost
slot.captured.onAvailable(mockk())
verify { observer.onWifiConnectionChanged(connected = true) }
slot.captured.onLost(mockk())
verify { observer.onWifiConnectionChanged(connected = false) }
}
private fun captureNetworkCallback(): ConnectivityManager.NetworkCallback {
val slot = slot<ConnectivityManager.NetworkCallback>()
verify { connectivityManager.registerNetworkCallback(any(), capture(slot)) }
return slot.captured
}
}

@ -3,5 +3,5 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
object AndroidComponents {
const val VERSION = "58.0.20200908130811"
const val VERSION = "59.0.20200911130559"
}

@ -45,8 +45,8 @@ object Config {
}
private val fennecBaseVersionCode by lazy {
val format = SimpleDateFormat("YYYYMMDDHHMMSS", Locale.US)
val cutoff = format.parse("20150801000000")
val format = SimpleDateFormat("yyyyMMddHHmmss", Locale.US)
val cutoff = format.parse("20141228000000")
val build = Date()
Math.floor((build.time - cutoff.time) / (1000.0 * 60.0 * 60.0)).toInt()
@ -56,6 +56,12 @@ object Config {
* Generates a versionCode that follows the same rules like legacy Fennec builds.
* Adapted from:
* https://searchfox.org/mozilla-central/rev/34cb8d0a2a324043bcfc2c56f37b31abe7fb23a8/python/mozbuild/mozbuild/android_version_code.py
*
* There is a discrepancy between the epoch date used here (20141228)
* and the epoch used in Fennec (20150801) for historical reasons. We keep
* this discrepancy to avoid having Fenix version codes decrease.
* Note that the original Fennec implementation also had an inconsistency in
* the documented epoch date (20150901) and the effective epoch date (20150801).
*/
@JvmStatic
fun generateFennecVersionCode(abi: String): Int {
@ -69,7 +75,7 @@ object Config {
// 0111 1000 0010 tttt tttt tttt tttt txpg
//
// The 17 bits labelled 't' represent the number of hours since midnight on
// September 1, 2015. (2015090100 in YYYYMMMDDHH format.) This yields a
// December 28, 2014. (2014122800 in yyyyMMddHH format.) This yields a
// little under 15 years worth of hourly build identifiers, since 2**17 / (366
// * 24) =~ 14.92.
//

@ -106,6 +106,7 @@ object Deps {
const val mozilla_feature_site_permissions = "org.mozilla.components:feature-sitepermissions:${Versions.mozilla_android_components}"
const val mozilla_feature_readerview = "org.mozilla.components:feature-readerview:${Versions.mozilla_android_components}"
const val mozilla_feature_tab_collections = "org.mozilla.components:feature-tab-collections:${Versions.mozilla_android_components}"
const val mozilla_feature_recentlyclosed = "org.mozilla.components:feature-recentlyclosed:${Versions.mozilla_android_components}"
const val mozilla_feature_accounts_push = "org.mozilla.components:feature-accounts-push:${Versions.mozilla_android_components}"
const val mozilla_feature_top_sites = "org.mozilla.components:feature-top-sites:${Versions.mozilla_android_components}"
const val mozilla_feature_share = "org.mozilla.components:feature-share:${Versions.mozilla_android_components}"

@ -0,0 +1,101 @@
#!/usr/bin/env python3
# Purpose: uplift (via cherry-picking) any missing commits from an l10n bot
# from 'MAIN_BRANCH' to a specified release branch.
#
# Usage examples: (append --verbose to print out detailed information)
# Dry-run (says what will happen, doesn't do any work): ./l10n-uplift.py releases/48.0
# Uplift, actually perform the work: ./l10n-uplift.py releases/48.0 --uplift
# Process multiple branches at once: ./l10n-uplift.py releases/48.0 releases/44.0 --uplift --verbose
# Note: there can often be conflicts between cherry-picks, to catch duplication errors, build after conflict resolution: ./gradlew assembleDebug
import subprocess
import argparse
# TODO don't forget to change this once we switch to 'main' or whatever other name.
MAIN_BRANCH="master"
L10N_AUTHOR="release+l10n-automation-bot@mozilla.com"
def run_cmd_checked(*args, **kwargs):
"""Run a command, throwing an exception if it exits with non-zero status."""
kwargs["check"] = True
kwargs["capture_output"] = True
# beware! only run this script with inputs from a trusted, non-external source
kwargs["shell"] = True
try:
return subprocess.run(*args, **kwargs).stdout.decode()
except subprocess.CalledProcessError as err:
print(err.stderr)
raise err
def uplift_commits(branch, verbose, uplift):
print(f"\nProcessing l10n commits for '{branch}'...")
# if necessary, this will setup 'branch' to track its upstream equivalent
run_cmd_checked([f"git checkout {branch}"])
# get l10n commits which happened on MAIN_BRANCH since 'branch' split off
commits_since_split = run_cmd_checked([f"git rev-list {branch}..{MAIN_BRANCH} --author={L10N_AUTHOR}"]).split()
# order commits by oldest-first, e.g. how we'd cherry pick them
commits_since_split.reverse()
print(f"Since '{branch}' split off '{MAIN_BRANCH}', there were {len(commits_since_split)} commit(s) from {L10N_AUTHOR}.")
if verbose:
print(f"\nHashes of those commits on '{MAIN_BRANCH}' are: {commits_since_split}\n")
# look for 'cherry picked' commits, and get the original commit hash from the commit message (as left by 'cherry-pick -x')
commits_already_uplifted = run_cmd_checked([f"git rev-list {MAIN_BRANCH}..{branch} --author={L10N_AUTHOR} --grep=\"cherry picked\" --pretty=%b | grep cherry | cut -d' ' -f5 | cut -c 1-40"]).split()
commits_already_uplifted.reverse()
print(f"Of those, {len(commits_already_uplifted)} commit(s) already uplifted.")
if verbose:
print(f"Hashes of commits already uplifted to '{branch}': {commits_already_uplifted}\n")
commits_to_uplift = [commit for commit in commits_since_split if commit not in commits_already_uplifted]
print(f"Need to uplift {len(commits_to_uplift)} commit(s).")
if verbose:
print(f"Hashes of commits to uplift from '{MAIN_BRANCH}' to '{branch}': {commits_to_uplift}\n")
if len(commits_to_uplift) == 0:
print("Nothing to uplift.")
return
if uplift:
print(f"Uplifting (for real)...")
else:
print(f"Uplifting (dry-run)...")
run_cmd_checked([f"git checkout {branch}"])
for commit in commits_to_uplift:
if verbose:
print(f"Cherry picking {commit} from '{MAIN_BRANCH}' to '{branch}'")
if uplift:
run_cmd_checked([f"git cherry-pick {commit} -x"])
if uplift:
print(f"Uplifted {len(commits_to_uplift)} commits from '{MAIN_BRANCH}' to '{branch}'")
parser = argparse.ArgumentParser(description=f"Uplift l10n commits from {MAIN_BRANCH} to specified branches")
parser.add_argument(
'branches', nargs='+', type=str,
help='target branches, e.g. specific release branches')
parser.add_argument(
'--verbose', default=False, action='store_true',
help='print out commit hashes and other detailed information'
)
parser.add_argument(
'--uplift', default=False, action='store_true',
help='uplift l10n commits missing from specified branches (if not specified, dry-run is performed)'
)
args = parser.parse_args()
# remember the current branch, so that we can return to it once we're done.
current_branch = run_cmd_checked(["git rev-parse --abbrev-ref HEAD"])
try:
for branch in args.branches:
uplift_commits(branch, args.verbose, args.uplift)
finally:
# go back to the branch we were on before 'uplift_for_branches' ran
run_cmd_checked([f"git checkout {current_branch}"])
Loading…
Cancel
Save