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

pull/21/head
Adam Novak 4 years ago
commit e8c354f0bb

@ -369,7 +369,6 @@ dependencies {
implementation Deps.androidx_coordinatorlayout
implementation Deps.sentry
implementation Deps.osslicenses_library
implementation Deps.leanplum_core
implementation Deps.leanplum_fcm

File diff suppressed because it is too large Load Diff

@ -10,13 +10,13 @@ import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.sendSingleTapToScreen
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.notificationShade
@ -148,7 +148,6 @@ class TabbedBrowsingTest {
}
@Test
@Ignore("For some reason this intermittently fails with the drawer :(")
fun closeTabTest() {
var genericURLS = TestAssetHelper.getGenericAssets(mockWebServer)
@ -161,25 +160,39 @@ class TabbedBrowsingTest {
closeTabViaXButton("Test_Page_${index + 1}")
verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO")
// verifyExistingOpenTabs("Test_Page_${index + 1}")
// verifyCloseTabsButton("Test_Page_${index + 1}")
// swipeTabRight("Test_Page_${index + 1}")
// verifySnackBarText("Tab closed")
// snackBarButtonClick("UNDO")
// verifyExistingOpenTabs("Test_Page_${index + 1}")
// verifyCloseTabsButton("Test_Page_${index + 1}")
// swipeTabLeft("Test_Page_${index + 1}")
// verifySnackBarText("Tab closed")
// snackBarButtonClick("UNDO")
}
mDevice.waitForIdle()
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}")
swipeTabRight("Test_Page_${index + 1}")
verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO")
}
mDevice.waitForIdle()
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}")
swipeTabLeft("Test_Page_${index + 1}")
verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO")
}
mDevice.waitForIdle()
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}")
verifyCloseTabsButton("Test_Page_${index + 1}")
}.openHomeScreen {
}
}
}
@Test
@Ignore("For some reason this intermittently fails with the drawer :(")
fun closePrivateTabTest() {
var genericURLS = TestAssetHelper.getGenericAssets(mockWebServer)
@ -193,19 +206,34 @@ class TabbedBrowsingTest {
closeTabViaXButton("Test_Page_${index + 1}")
verifySnackBarText("Private tab closed")
snackBarButtonClick("UNDO")
// verifyExistingOpenTabs("Test_Page_${index + 1}")
// verifyCloseTabsButton("Test_Page_${index + 1}")
// swipeTabRight("Test_Page_${index + 1}")
// verifySnackBarText("Private tab closed")
// snackBarButtonClick("UNDO")
// verifyExistingOpenTabs("Test_Page_${index + 1}")
// verifyCloseTabsButton("Test_Page_${index + 1}")
// swipeTabLeft("Test_Page_${index + 1}")
// verifySnackBarText("Private tab closed")
// snackBarButtonClick("UNDO")
}
mDevice.waitForIdle()
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}")
swipeTabRight("Test_Page_${index + 1}")
verifySnackBarText("Private tab closed")
snackBarButtonClick("UNDO")
}
mDevice.waitForIdle()
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}")
verifyCloseTabsButton("Test_Page_${index + 1}")
}.openHomeScreen {
swipeTabLeft("Test_Page_${index + 1}")
verifySnackBarText("Private tab closed")
snackBarButtonClick("UNDO")
}
mDevice.waitForIdle()
browserScreen {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_${index + 1}")
closeTabViaXButton("Test_Page_${index + 1}")
}
}
}

@ -228,13 +228,8 @@
<activity android:name=".settings.account.AuthIntentReceiverActivity"
android:exported="false" />
<activity android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.DayNight.DarkActionBar"/>
<activity android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.DayNight.DarkActionBar"/>
<activity android:name=".settings.about.AboutLibrariesActivity"
android:exported="false" />
<service android:name=".media.MediaService"
android:exported="false" />
@ -260,7 +255,7 @@
android:resource="@xml/search_widget_info" />
</receiver>
<service android:name=".session.SessionNotificationService"
<service android:name=".session.PrivateNotificationService"
android:exported="false" />
<service

@ -44,6 +44,7 @@ import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate
import mozilla.components.feature.privatemode.notification.PrivateNotificationFeature
import mozilla.components.feature.search.BrowserStoreSearchAdapter
import mozilla.components.feature.search.SearchAdapter
import mozilla.components.service.fxa.sync.SyncReason
@ -88,7 +89,7 @@ import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.search.SearchFragmentDirections
import org.mozilla.fenix.searchdialog.SearchDialogFragmentDirections
import org.mozilla.fenix.session.NotificationSessionObserver
import org.mozilla.fenix.session.PrivateNotificationService
import org.mozilla.fenix.settings.SettingsFragmentDirections
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
import org.mozilla.fenix.settings.about.AboutFragmentDirections
@ -121,7 +122,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
private var isVisuallyComplete = false
private var visualCompletenessQueue: RunWhenReadyQueue? = null
private var privateNotificationObserver: NotificationSessionObserver? = null
private var privateNotificationObserver: PrivateNotificationFeature<PrivateNotificationService>? = null
private var isToolbarInflated = false
@ -174,7 +175,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
sessionObserver = UriOpenedObserver(this)
checkPrivateShortcutEntryPoint(intent)
privateNotificationObserver = NotificationSessionObserver(applicationContext).also {
privateNotificationObserver = PrivateNotificationFeature(
applicationContext,
components.core.store,
PrivateNotificationService::class
).also {
it.start()
}
@ -479,7 +484,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
intent.getStringExtra(OPEN_TO_SEARCH) ==
StartSearchIntentProcessor.PRIVATE_BROWSING_PINNED_SHORTCUT)
) {
NotificationSessionObserver.isStartedFromPrivateShortcut = true
PrivateNotificationService.isStartedFromPrivateShortcut = true
}
}

@ -18,10 +18,10 @@ import com.google.android.material.switchmaterial.SwitchMaterial
import kotlinx.android.synthetic.main.fragment_installed_add_on_details.view.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManagerException
import mozilla.components.feature.addons.ui.translatedName
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.showToolbar
@ -189,8 +189,7 @@ class InstalledAddonDetailsFragment : Fragment() {
val directions = if (addon.installedState?.openOptionsPageInTab == true) {
val components = it.context.components
val shouldCreatePrivateSession =
components.core.store.state.selectedTab?.content?.private
?: components.settings.openLinksInAPrivateTab
(activity as HomeActivity).browsingModeManager.mode.isPrivate
if (shouldCreatePrivateSession) {
components.useCases.tabsUseCases.addPrivateTab(settingUrl)

@ -189,6 +189,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
browserInitialized = initializeUI(view) != null
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
}
@Suppress("ComplexMethod", "LongMethod")
@ -439,7 +440,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
sessionManager = sessionManager,
sessionId = customTabSessionId,
fragmentManager = parentFragmentManager,
launchInApp = { context.settings().openLinksInExternalApp }
launchInApp = { context.settings().openLinksInExternalApp },
loadUrlUseCase = context.components.useCases.sessionUseCases.loadUrl
),
owner = this,
view = view
@ -756,7 +758,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
super.onStart()
requireComponents.core.sessionManager.register(this, this, autoPause = true)
sitePermissionWifiIntegration.get()?.maybeAddWifiConnectedListener()
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
}
@CallSuper
@ -1062,9 +1063,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
*/
override fun onDestroyView() {
super.onDestroyView()
requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this)
_browserToolbarView = null
_browserInteractor = null
requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this)
}
companion object {

@ -23,6 +23,7 @@ import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.support.ktx.android.util.dpToPx
import mozilla.components.support.ktx.android.view.getRectWithViewLocation
import org.mozilla.fenix.ext.getRectWithScreenLocation
import org.mozilla.fenix.ext.getWindowInsets
import org.mozilla.fenix.ext.isKeyboardVisible
import org.mozilla.fenix.ext.sessionsOfType
@ -304,7 +305,7 @@ class ToolbarGestureHandler(
}
private fun PointF.isInToolbar(): Boolean {
val toolbarLocation = toolbarLayout.getRectWithViewLocation()
val toolbarLocation = toolbarLayout.getRectWithScreenLocation()
// In Android 10, the system gesture touch area overlaps the bottom of the toolbar, so
// lets make our swipe area taller by that amount
activity.window.decorView.getWindowInsets()?.let { insets ->

@ -27,7 +27,10 @@ class DefaultReaderModeController(
private val isPrivate: Boolean = false
) : ReaderModeController {
override fun hideReaderView() {
readerViewFeature.withFeature { it.hideReaderView() }
readerViewFeature.withFeature {
it.hideReaderView()
it.hideControls()
}
}
override fun showReaderView() {

@ -4,7 +4,10 @@
package org.mozilla.fenix.components
import androidx.annotation.VisibleForTesting
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicyForSessionTypes
import org.mozilla.fenix.Config
import org.mozilla.fenix.utils.Settings
/**
@ -34,9 +37,9 @@ class TrackingProtectionPolicyFactory(private val settings: Settings) {
}
return when {
normalMode && privateMode -> trackingProtectionPolicy
normalMode && !privateMode -> trackingProtectionPolicy.forRegularSessionsOnly()
!normalMode && privateMode -> trackingProtectionPolicy.forPrivateSessionsOnly()
normalMode && privateMode -> trackingProtectionPolicy.adaptPolicyToChannel()
normalMode && !privateMode -> trackingProtectionPolicy.adaptPolicyToChannel().forRegularSessionsOnly()
!normalMode && privateMode -> trackingProtectionPolicy.adaptPolicyToChannel().forPrivateSessionsOnly()
else -> TrackingProtectionPolicy.none()
}
}
@ -44,7 +47,8 @@ class TrackingProtectionPolicyFactory(private val settings: Settings) {
private fun createCustomTrackingProtectionPolicy(): TrackingProtectionPolicy {
return TrackingProtectionPolicy.select(
cookiePolicy = getCustomCookiePolicy(),
trackingCategories = getCustomTrackingCategories()
trackingCategories = getCustomTrackingCategories(),
cookiePurging = Config.channel.isNightlyOrDebug
).let {
if (settings.blockTrackingContentSelectionInCustomTrackingProtection == "private") {
it.forPrivateSessionsOnly()
@ -91,3 +95,13 @@ class TrackingProtectionPolicyFactory(private val settings: Settings) {
return categories.toTypedArray()
}
}
@VisibleForTesting
internal fun TrackingProtectionPolicyForSessionTypes.adaptPolicyToChannel(): TrackingProtectionPolicyForSessionTypes {
return TrackingProtectionPolicy.select(
trackingCategories = trackingCategories,
cookiePolicy = cookiePolicy,
strictSocialTrackingProtection = strictSocialTrackingProtection,
cookiePurging = Config.channel.isNightlyOrDebug
)
}

@ -102,10 +102,13 @@ class FennecWebAppIntentProcessor(
internal fun fromFile(path: String?): WebAppManifest? {
if (path.isNullOrEmpty()) return null
val file = File(path)
if (!file.isUnderFennecManifestDirectory()) return null
return try {
// Gecko in Fennec added some add some additional data, such as cached_icon, in
// the toplevel object. The actual web app manifest is in the "manifest" field.
val manifest = JSONObject(File(path).readText())
val manifest = JSONObject(file.readText())
val manifestField = manifest.getJSONObject("manifest")
WebAppManifestParser().parse(manifestField).getOrNull()
@ -114,12 +117,27 @@ class FennecWebAppIntentProcessor(
}
}
/**
* Fennec manifests should be located in <filesDir>/mozilla/<profile>/manifests/
*/
private fun File.isUnderFennecManifestDirectory(): Boolean {
val manifestsDir = canonicalFile.parentFile
// Check that manifest is in a folder named "manifests"
return manifestsDir == null || manifestsDir.name != "manifests" ||
// Check that the folder two levels up is named "mozilla"
manifestsDir.parentFile?.parentFile != getMozillaDirectory()
}
private fun createFallbackCustomTabConfig(): CustomTabConfig {
return CustomTabConfig(
toolbarColor = ContextCompat.getColor(context, R.color.toolbar_center_gradient_normal_theme)
)
}
private fun getMozillaDirectory(): File {
return File(context.filesDir, "mozilla")
}
companion object {
const val ACTION_FENNEC_WEBAPP = "org.mozilla.gecko.WEBAPP"
const val EXTRA_FENNEC_MANIFEST_PATH = "MANIFEST_PATH"

@ -31,6 +31,20 @@ fun View.removeTouchDelegate() {
}
}
/**
* Fills a [Rect] with data about a view's location in the screen.
*
* @see View.getLocationOnScreen
* @see View.getRectWithViewLocation for a version of this that is relative to a window
*/
fun View.getRectWithScreenLocation(): Rect {
val locationOnScreen = IntArray(2).apply { getLocationOnScreen(this) }
return Rect(locationOnScreen[0],
locationOnScreen[1],
locationOnScreen[0] + width,
locationOnScreen[1] + height)
}
/**
* A safer version of [ViewCompat.getRootWindowInsets] that does not throw a NullPointerException
* if the view is not attached.

@ -146,7 +146,9 @@ class HomeFragment : Fragment() {
private val store: BrowserStore
get() = requireComponents.core.store
private val onboarding by lazy { FenixOnboarding(requireContext()) }
private val onboarding by lazy { StrictMode.allowThreadDiskReads().resetPoliciesAfter {
FenixOnboarding(requireContext()) } }
private lateinit var homeFragmentStore: HomeFragmentStore
private var _sessionControlInteractor: SessionControlInteractor? = null
protected val sessionControlInteractor: SessionControlInteractor

@ -13,8 +13,8 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.library_site_item.view.*
import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.concept.menu.MenuController
import mozilla.components.concept.menu.Orientation
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.increaseTapArea
@ -48,10 +48,6 @@ interface SelectionHolder<T> {
val selectedItems: Set<T>
}
interface LibraryItemMenu {
val menuBuilder: BrowserMenuBuilder
}
class LibrarySiteItemView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@ -102,11 +98,11 @@ class LibrarySiteItemView @JvmOverloads constructor(
context.components.core.icons.loadIntoView(favicon, url)
}
fun attachMenu(menu: LibraryItemMenu) {
fun attachMenu(menuController: MenuController) {
overflow_menu.setOnClickListener {
menu.menuBuilder.build(context).show(
menuController.show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN
orientation = Orientation.DOWN
)
}
}

@ -7,6 +7,7 @@ package org.mozilla.fenix.library.bookmarks
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
@ -15,18 +16,16 @@ import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.SelectionHolder
import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkFolderViewHolder
import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkItemViewHolder
import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkNodeViewHolder
import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkSeparatorViewHolder
class BookmarkAdapter(val emptyView: View, val interactor: BookmarkViewInteractor) :
RecyclerView.Adapter<BookmarkNodeViewHolder>(), SelectionHolder<BookmarkNode> {
class BookmarkAdapter(private val emptyView: View, private val interactor: BookmarkViewInteractor) :
RecyclerView.Adapter<BookmarkNodeViewHolder>() {
private var tree: List<BookmarkNode> = listOf()
private var mode: BookmarkFragmentState.Mode = BookmarkFragmentState.Mode.Normal()
override val selectedItems: Set<BookmarkNode> get() = mode.selectedItems
private var isFirstRun = true
fun updateData(tree: BookmarkNode?, mode: BookmarkFragmentState.Mode) {
@ -49,7 +48,8 @@ class BookmarkAdapter(val emptyView: View, val interactor: BookmarkViewInteracto
diffUtil.dispatchUpdatesTo(this)
}
private class BookmarkDiffUtil(
@VisibleForTesting
internal class BookmarkDiffUtil(
val old: List<BookmarkNode>,
val new: List<BookmarkNode>,
val oldMode: BookmarkFragmentState.Mode,
@ -59,9 +59,20 @@ class BookmarkAdapter(val emptyView: View, val interactor: BookmarkViewInteracto
old[oldItemPosition].guid == new[newItemPosition].guid
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldMode::class == newMode::class &&
old[oldItemPosition] in oldMode.selectedItems == new[newItemPosition] in newMode.selectedItems &&
old[oldItemPosition].title == new[newItemPosition].title &&
old[oldItemPosition].url == new[newItemPosition].url
old[oldItemPosition] == new[newItemPosition]
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldItem = old[oldItemPosition]
val newItem = new[newItemPosition]
return BookmarkPayload(
titleChanged = oldItem.title != newItem.title,
urlChanged = oldItem.url != newItem.url,
selectedChanged = oldItem in oldMode.selectedItems != newItem in newMode.selectedItems,
modeChanged = oldMode::class != newMode::class
)
}
override fun getOldListSize(): Int = old.size
override fun getNewListSize(): Int = new.size
@ -72,8 +83,8 @@ class BookmarkAdapter(val emptyView: View, val interactor: BookmarkViewInteracto
.inflate(R.layout.bookmark_list_item, parent, false) as LibrarySiteItemView
return when (viewType) {
LibrarySiteItemView.ItemType.SITE.ordinal -> BookmarkItemViewHolder(view, interactor, this)
LibrarySiteItemView.ItemType.FOLDER.ordinal -> BookmarkFolderViewHolder(view, interactor, this)
LibrarySiteItemView.ItemType.SITE.ordinal -> BookmarkItemViewHolder(view, interactor)
LibrarySiteItemView.ItemType.FOLDER.ordinal -> BookmarkFolderViewHolder(view, interactor)
LibrarySiteItemView.ItemType.SEPARATOR.ordinal -> BookmarkSeparatorViewHolder(view, interactor)
else -> throw IllegalStateException("ViewType $viewType does not match to a ViewHolder")
}
@ -90,9 +101,36 @@ class BookmarkAdapter(val emptyView: View, val interactor: BookmarkViewInteracto
override fun getItemCount(): Int = tree.size
override fun onBindViewHolder(
holder: BookmarkNodeViewHolder,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.isNotEmpty() && payloads[0] is BookmarkPayload) {
holder.bind(tree[position], mode, payloads[0] as BookmarkPayload)
} else {
super.onBindViewHolder(holder, position, payloads)
}
}
override fun onBindViewHolder(holder: BookmarkNodeViewHolder, position: Int) {
holder.bind(tree[position], mode)
}
}
/**
* A RecyclerView Adapter payload class that contains information about changes to a [BookmarkNode].
*
* @property titleChanged true if there has been a change to [BookmarkNode.title].
* @property urlChanged true if there has been a change to [BookmarkNode.url].
* @property selectedChanged true if there has been a change in the BookmarkNode's selected state.
* @property modeChanged true if there has been a change in the state's mode type.
*/
data class BookmarkPayload(
val titleChanged: Boolean,
val urlChanged: Boolean,
val selectedChanged: Boolean,
val modeChanged: Boolean
)
fun BookmarkNode.inRoots() = enumValues<BookmarkRoot>().any { it.id == guid }

@ -9,6 +9,7 @@ import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import org.mozilla.fenix.library.SelectionHolder
class BookmarkFragmentStore(
initialState: BookmarkFragmentState
@ -32,8 +33,8 @@ data class BookmarkFragmentState(
val isLoading: Boolean = true,
val isSwipeToRefreshEnabled: Boolean = true
) : State {
sealed class Mode {
open val selectedItems = emptySet<BookmarkNode>()
sealed class Mode : SelectionHolder<BookmarkNode> {
override val selectedItems = emptySet<BookmarkNode>()
data class Normal(val showMenu: Boolean = true) : Mode()
data class Selecting(override val selectedItems: Set<BookmarkNode>) : Mode()

@ -5,65 +5,89 @@
package org.mozilla.fenix.library.bookmarks
import android.content.Context
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.concept.storage.BookmarkNode
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.menu2.BrowserMenuController
import mozilla.components.concept.menu.MenuController
import mozilla.components.concept.menu.candidate.TextMenuCandidate
import mozilla.components.concept.menu.candidate.TextStyle
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibraryItemMenu
import org.mozilla.fenix.theme.ThemeManager
class BookmarkItemMenu(
private val context: Context,
private val item: BookmarkNode,
private val onItemTapped: (BookmarkItemMenu.Item) -> Unit = {}
) : LibraryItemMenu {
private val onItemTapped: (Item) -> Unit
) {
sealed class Item {
object Edit : Item()
object Select : Item()
object Copy : Item()
object Share : Item()
object OpenInNewTab : Item()
object OpenInPrivateTab : Item()
object Delete : Item()
enum class Item {
Edit,
Copy,
Share,
OpenInNewTab,
OpenInPrivateTab,
Delete;
}
override val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
val menuController: MenuController by lazy { BrowserMenuController() }
private val menuItems by lazy {
listOfNotNull(
if (item.type in listOf(BookmarkNodeType.ITEM, BookmarkNodeType.FOLDER)) {
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_edit_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Edit)
@VisibleForTesting
internal fun menuItems(itemType: BookmarkNodeType): List<TextMenuCandidate> {
return listOfNotNull(
if (itemType != BookmarkNodeType.SEPARATOR) {
TextMenuCandidate(
text = context.getString(R.string.bookmark_menu_edit_button)
) {
onItemTapped.invoke(Item.Edit)
}
} else null,
if (item.type == BookmarkNodeType.ITEM) {
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_copy_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Copy)
} else {
null
},
if (itemType == BookmarkNodeType.ITEM) {
TextMenuCandidate(
text = context.getString(R.string.bookmark_menu_copy_button)
) {
onItemTapped.invoke(Item.Copy)
}
} else null,
if (item.type == BookmarkNodeType.ITEM) {
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_share_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.Share)
} else {
null
},
if (itemType == BookmarkNodeType.ITEM) {
TextMenuCandidate(
text = context.getString(R.string.bookmark_menu_share_button)
) {
onItemTapped.invoke(Item.Share)
}
} else null,
if (item.type == BookmarkNodeType.ITEM) {
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_open_in_new_tab_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.OpenInNewTab)
} else {
null
},
if (itemType == BookmarkNodeType.ITEM) {
TextMenuCandidate(
text = context.getString(R.string.bookmark_menu_open_in_new_tab_button)
) {
onItemTapped.invoke(Item.OpenInNewTab)
}
} else null,
if (item.type == BookmarkNodeType.ITEM) {
SimpleBrowserMenuItem(context.getString(R.string.bookmark_menu_open_in_private_tab_button)) {
onItemTapped.invoke(BookmarkItemMenu.Item.OpenInPrivateTab)
} else {
null
},
if (itemType == BookmarkNodeType.ITEM) {
TextMenuCandidate(
text = context.getString(R.string.bookmark_menu_open_in_private_tab_button)
) {
onItemTapped.invoke(Item.OpenInPrivateTab)
}
} else null,
SimpleBrowserMenuItem(
context.getString(R.string.bookmark_menu_delete_button),
textColorResource = ThemeManager.resolveAttribute(R.attr.destructive, context)
} else {
null
},
TextMenuCandidate(
text = context.getString(R.string.bookmark_menu_delete_button),
textStyle = TextStyle(color = context.getColorFromAttr(R.attr.destructive))
) {
onItemTapped.invoke(BookmarkItemMenu.Item.Delete)
onItemTapped.invoke(Item.Delete)
}
)
}
fun updateMenu(itemType: BookmarkNodeType) {
menuController.submitList(menuItems(itemType))
}
}

@ -139,7 +139,6 @@ class BookmarkView(
}
fun update(state: BookmarkFragmentState) {
val oldMode = mode
tree = state.tree
if (state.mode != mode) {
mode = state.mode
@ -149,9 +148,6 @@ class BookmarkView(
}
bookmarkAdapter.updateData(state.tree, mode)
if (state.mode != oldMode) {
bookmarkAdapter.notifyDataSetChanged()
}
when (mode) {
is BookmarkFragmentState.Mode.Normal -> {

@ -12,8 +12,8 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.SelectionHolder
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
import org.mozilla.fenix.library.bookmarks.BookmarkPayload
import org.mozilla.fenix.library.bookmarks.BookmarkViewInteractor
import org.mozilla.fenix.library.bookmarks.inRoots
@ -22,34 +22,44 @@ import org.mozilla.fenix.library.bookmarks.inRoots
*/
class BookmarkFolderViewHolder(
view: LibrarySiteItemView,
interactor: BookmarkViewInteractor,
private val selectionHolder: SelectionHolder<BookmarkNode>
interactor: BookmarkViewInteractor
) : BookmarkNodeViewHolder(view, interactor) {
override var item: BookmarkNode? = null
init {
containerView.displayAs(LibrarySiteItemView.ItemType.FOLDER)
}
override fun bind(
item: BookmarkNode,
mode: BookmarkFragmentState.Mode
) {
this.item = item
bind(item, mode, BookmarkPayload(true, true, true, true))
}
containerView.displayAs(LibrarySiteItemView.ItemType.FOLDER)
override fun bind(item: BookmarkNode, mode: BookmarkFragmentState.Mode, payload: BookmarkPayload) {
this.item = item
setSelectionListeners(item, selectionHolder)
setSelectionListeners(item, mode)
if (!item.inRoots()) {
setupMenu(item)
if (mode is BookmarkFragmentState.Mode.Selecting) {
containerView.overflowView.hideAndDisable()
} else {
containerView.overflowView.showAndEnable()
updateMenu(item.type)
if (payload.modeChanged) {
if (mode is BookmarkFragmentState.Mode.Selecting) {
containerView.overflowView.hideAndDisable()
} else {
containerView.overflowView.showAndEnable()
}
}
} else {
containerView.overflowView.visibility = View.GONE
}
containerView.changeSelected(item in selectionHolder.selectedItems)
if (payload.selectedChanged) {
containerView.changeSelected(item in mode.selectedItems)
}
containerView.iconView.setImageDrawable(
AppCompatResources.getDrawable(
containerView.context,
@ -63,6 +73,9 @@ class BookmarkFolderViewHolder(
)
}
)
containerView.titleView.text = item.title
if (payload.titleChanged) {
containerView.titleView.text = item.title
}
}
}

@ -4,12 +4,13 @@
package org.mozilla.fenix.library.bookmarks.viewholders
import androidx.annotation.VisibleForTesting
import mozilla.components.concept.storage.BookmarkNode
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.SelectionHolder
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
import org.mozilla.fenix.library.bookmarks.BookmarkPayload
import org.mozilla.fenix.library.bookmarks.BookmarkViewInteractor
/**
@ -17,36 +18,55 @@ import org.mozilla.fenix.library.bookmarks.BookmarkViewInteractor
*/
class BookmarkItemViewHolder(
view: LibrarySiteItemView,
interactor: BookmarkViewInteractor,
private val selectionHolder: SelectionHolder<BookmarkNode>
interactor: BookmarkViewInteractor
) : BookmarkNodeViewHolder(view, interactor) {
override var item: BookmarkNode? = null
init {
containerView.displayAs(LibrarySiteItemView.ItemType.SITE)
}
override fun bind(
item: BookmarkNode,
mode: BookmarkFragmentState.Mode
) {
bind(item, mode, BookmarkPayload(true, true, true, true))
}
override fun bind(item: BookmarkNode, mode: BookmarkFragmentState.Mode, payload: BookmarkPayload) {
this.item = item
containerView.displayAs(LibrarySiteItemView.ItemType.SITE)
updateMenu(item.type)
if (mode is BookmarkFragmentState.Mode.Selecting) {
containerView.overflowView.hideAndDisable()
} else {
containerView.overflowView.showAndEnable()
if (payload.modeChanged) {
if (mode is BookmarkFragmentState.Mode.Selecting) {
containerView.overflowView.hideAndDisable()
} else {
containerView.overflowView.showAndEnable()
}
}
if (payload.selectedChanged) {
containerView.changeSelected(item in mode.selectedItems)
}
setupMenu(item)
containerView.titleView.text = if (item.title.isNullOrBlank()) item.url else item.title
containerView.urlView.text = item.url
setSelectionListeners(item, selectionHolder)
if (payload.titleChanged) {
containerView.titleView.text = if (item.title.isNullOrBlank()) item.url else item.title
} else if (payload.urlChanged && item.title.isNullOrBlank()) {
containerView.titleView.text = item.url
}
if (payload.urlChanged) {
containerView.urlView.text = item.url
setColorsAndIcons(item.url)
}
containerView.changeSelected(item in selectionHolder.selectedItems)
setColorsAndIcons(item.url)
setSelectionListeners(item, mode)
}
private fun setColorsAndIcons(url: String?) {
@VisibleForTesting
internal fun setColorsAndIcons(url: String?) {
if (url != null && url.startsWith("http")) {
containerView.loadFavicon(url)
} else {

@ -5,38 +5,48 @@
package org.mozilla.fenix.library.bookmarks.viewholders
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.SelectionHolder
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
import org.mozilla.fenix.library.bookmarks.BookmarkItemMenu
import org.mozilla.fenix.library.bookmarks.BookmarkPayload
import org.mozilla.fenix.library.bookmarks.BookmarkViewInteractor
import org.mozilla.fenix.utils.Do
/**
* Base class for bookmark node view holders.
*/
abstract class BookmarkNodeViewHolder(
override val containerView: LibrarySiteItemView,
protected val containerView: LibrarySiteItemView,
private val interactor: BookmarkViewInteractor
) : RecyclerView.ViewHolder(containerView), LayoutContainer {
) : RecyclerView.ViewHolder(containerView) {
abstract var item: BookmarkNode?
private lateinit var menu: BookmarkItemMenu
init {
setupMenu()
}
abstract fun bind(item: BookmarkNode, mode: BookmarkFragmentState.Mode)
abstract fun bind(
item: BookmarkNode,
mode: BookmarkFragmentState.Mode
mode: BookmarkFragmentState.Mode,
payload: BookmarkPayload
)
protected fun setSelectionListeners(item: BookmarkNode, selectionHolder: SelectionHolder<BookmarkNode>) {
containerView.setSelectionInteractor(item, selectionHolder, interactor)
}
protected fun setupMenu(item: BookmarkNode) {
val bookmarkItemMenu = BookmarkItemMenu(containerView.context, item) {
when (it) {
private fun setupMenu() {
menu = BookmarkItemMenu(containerView.context) { menuItem ->
val item = this.item ?: return@BookmarkItemMenu
Do exhaustive when (menuItem) {
BookmarkItemMenu.Item.Edit -> interactor.onEditPressed(item)
BookmarkItemMenu.Item.Select -> interactor.select(item)
BookmarkItemMenu.Item.Copy -> interactor.onCopyPressed(item)
BookmarkItemMenu.Item.Share -> interactor.onSharePressed(item)
BookmarkItemMenu.Item.OpenInNewTab -> interactor.onOpenInNormalTab(item)
@ -45,6 +55,8 @@ abstract class BookmarkNodeViewHolder(
}
}
containerView.attachMenu(bookmarkItemMenu)
containerView.attachMenu(menu.menuController)
}
protected fun updateMenu(itemType: BookmarkNodeType) = menu.updateMenu(itemType)
}

@ -7,6 +7,7 @@ package org.mozilla.fenix.library.bookmarks.viewholders
import mozilla.components.concept.storage.BookmarkNode
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
import org.mozilla.fenix.library.bookmarks.BookmarkPayload
import org.mozilla.fenix.library.bookmarks.BookmarkViewInteractor
/**
@ -25,6 +26,14 @@ class BookmarkSeparatorViewHolder(
) {
this.item = item
containerView.displayAs(LibrarySiteItemView.ItemType.SEPARATOR)
setupMenu(item)
updateMenu(item.type)
}
override fun bind(
item: BookmarkNode,
mode: BookmarkFragmentState.Mode,
payload: BookmarkPayload
) {
bind(item, mode)
}
}

@ -5,43 +5,61 @@
package org.mozilla.fenix.library.history
import android.content.Context
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.menu2.BrowserMenuController
import mozilla.components.concept.menu.MenuController
import mozilla.components.concept.menu.candidate.TextMenuCandidate
import mozilla.components.concept.menu.candidate.TextStyle
import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibraryItemMenu
import org.mozilla.fenix.theme.ThemeManager
class HistoryItemMenu(
private val context: Context,
private val onItemTapped: (Item) -> Unit = {}
) : LibraryItemMenu {
sealed class Item {
object Copy : Item()
object Share : Item()
object OpenInNewTab : Item()
object OpenInPrivateTab : Item()
object Delete : Item()
private val onItemTapped: (Item) -> Unit
) {
enum class Item {
Copy,
Share,
OpenInNewTab,
OpenInPrivateTab,
Delete;
}
override val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
val menuController: MenuController by lazy {
BrowserMenuController().apply {
submitList(menuItems())
}
}
private val menuItems by lazy {
listOfNotNull(
SimpleBrowserMenuItem(context.getString(R.string.history_menu_copy_button)) {
@VisibleForTesting
internal fun menuItems(): List<TextMenuCandidate> {
return listOf(
TextMenuCandidate(
text = context.getString(R.string.history_menu_copy_button)
) {
onItemTapped.invoke(Item.Copy)
},
SimpleBrowserMenuItem(context.getString(R.string.history_menu_share_button)) {
TextMenuCandidate(
text = context.getString(R.string.history_menu_share_button)
) {
onItemTapped.invoke(Item.Share)
},
SimpleBrowserMenuItem(context.getString(R.string.history_menu_open_in_new_tab_button)) {
TextMenuCandidate(
text = context.getString(R.string.history_menu_open_in_new_tab_button)
) {
onItemTapped.invoke(Item.OpenInNewTab)
},
SimpleBrowserMenuItem(context.getString(R.string.history_menu_open_in_private_tab_button)) {
TextMenuCandidate(
text = context.getString(R.string.history_menu_open_in_private_tab_button)
) {
onItemTapped.invoke(Item.OpenInPrivateTab)
},
SimpleBrowserMenuItem(
context.getString(R.string.history_delete_item),
textColorResource = ThemeManager.resolveAttribute(R.attr.destructive, context)
TextMenuCandidate(
text = context.getString(R.string.history_delete_item),
textStyle = TextStyle(
color = context.getColorFromAttr(R.attr.destructive)
)
) {
onItemTapped.invoke(Item.Delete)
}

@ -110,7 +110,6 @@ class HistoryListItemViewHolder(
private fun setupMenu() {
val historyMenu = HistoryItemMenu(itemView.context) {
val item = this.item ?: return@HistoryItemMenu
Do exhaustive when (it) {
HistoryItemMenu.Item.Copy -> historyInteractor.onCopyPressed(item)
HistoryItemMenu.Item.Share -> historyInteractor.onSharePressed(item)
@ -120,7 +119,7 @@ class HistoryListItemViewHolder(
}
}
itemView.history_layout.attachMenu(historyMenu)
itemView.history_layout.attachMenu(historyMenu.menuController)
}
companion object {

@ -30,7 +30,6 @@ import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.android.synthetic.main.fragment_search.view.*
import kotlinx.android.synthetic.main.search_suggestions_onboarding.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.qr.QrFeature
@ -55,6 +54,7 @@ import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.search.awesomebar.AwesomeBarView
import org.mozilla.fenix.search.ext.areShortcutsAvailable
import org.mozilla.fenix.search.toolbar.ToolbarView
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.registerOnSharedPreferenceChangeListener
@ -71,14 +71,6 @@ class SearchFragment : Fragment(), UserInteractionHandler {
private val speechIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
private fun shouldShowSearchSuggestions(isPrivate: Boolean): Boolean =
if (isPrivate) {
requireContext().settings().shouldShowSearchSuggestions &&
requireContext().settings().shouldShowSearchSuggestionsInPrivate
} else {
requireContext().settings().shouldShowSearchSuggestions
}
@Suppress("LongMethod")
override fun onCreateView(
inflater: LayoutInflater,
@ -89,38 +81,18 @@ class SearchFragment : Fragment(), UserInteractionHandler {
val settings = activity.settings()
val args by navArgs<SearchFragmentArgs>()
val tabId = args.sessionId
val tab = tabId?.let { requireComponents.core.store.state.findTab(it) }
val view = inflater.inflate(R.layout.fragment_search, container, false)
val url = tab?.content?.url.orEmpty()
val currentSearchEngine = SearchEngineSource.Default(
requireComponents.search.provider.getDefaultEngine(requireContext())
)
val isPrivate = activity.browsingModeManager.mode.isPrivate
requireComponents.analytics.metrics.track(Event.InteractWithSearchURLArea)
val areShortcutsAvailable = areShortcutsAvailable()
searchStore = StoreProvider.get(this) {
SearchFragmentStore(
SearchFragmentState(
query = url,
url = url,
searchTerms = tab?.content?.searchTerms.orEmpty(),
searchEngineSource = currentSearchEngine,
defaultEngineSource = currentSearchEngine,
showSearchSuggestions = shouldShowSearchSuggestions(isPrivate),
showSearchSuggestionsHint = false,
showSearchShortcuts = settings.shouldShowSearchShortcuts &&
url.isEmpty() &&
areShortcutsAvailable,
areShortcutsAvailable = areShortcutsAvailable,
showClipboardSuggestions = settings.shouldShowClipboardSuggestions,
showHistorySuggestions = settings.shouldShowHistorySuggestions,
showBookmarkSuggestions = settings.shouldShowBookmarkSuggestions,
tabId = tabId,
createInitialSearchFragmentState(
activity,
requireComponents,
tabId = args.sessionId,
pastedText = args.pastedText,
searchAccessPoint = args.searchAccessPoint
)
@ -165,7 +137,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
ContextCompat.getDrawable(requireContext(), R.drawable.ic_microphone)!!,
requireContext().getString(R.string.voice_search_content_description),
visible = {
currentSearchEngine.searchEngine.identifier.contains("google") &&
searchStore.state.searchEngineSource.searchEngine.identifier.contains("google") &&
speechIsAvailable() &&
settings.shouldShowVoiceSearch
},
@ -343,10 +315,11 @@ class SearchFragment : Fragment(), UserInteractionHandler {
override fun onResume() {
super.onResume()
val provider = requireComponents.search.provider
// The user has the option to go to 'Shortcuts' -> 'Search engine settings' to modify the default search engine.
// When returning from that settings screen we need to update it to account for any changes.
val currentDefaultEngine =
requireComponents.search.provider.getDefaultEngine(requireContext())
val currentDefaultEngine = provider.getDefaultEngine(requireContext())
if (searchStore.state.defaultEngineSource.searchEngine != currentDefaultEngine) {
searchStore.dispatch(
@ -356,7 +329,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
}
// Users can from this fragment go to install/uninstall search engines and then return.
val areShortcutsAvailable = areShortcutsAvailable()
val areShortcutsAvailable = provider.areShortcutsAvailable(requireContext())
if (searchStore.state.areShortcutsAvailable != areShortcutsAvailable) {
searchStore.dispatch(SearchFragmentAction.UpdateShortcutsAvailability(areShortcutsAvailable))
}
@ -367,7 +340,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
updateClipboardSuggestion(
searchStore.state,
requireContext().components.clipboardHandler.url
requireComponents.clipboardHandler.url
)
permissionDidUpdate = false
@ -470,16 +443,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
}
}
/**
* Return if the user has *at least 2* installed search engines.
* Useful to decide whether to show / enable certain functionalities.
*/
private fun areShortcutsAvailable() =
requireContext().components.search.provider.installedSearchEngines(requireContext())
.list.size >= MINIMUM_SEARCH_ENGINES_NUMBER_TO_SHOW_SHORTCUTS
companion object {
private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1
private const val MINIMUM_SEARCH_ENGINES_NUMBER_TO_SHOW_SHORTCUTS = 2
}
}

@ -5,10 +5,15 @@
package org.mozilla.fenix.search
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.state.selector.findTab
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.search.ext.areShortcutsAvailable
/**
* The [Store] for holding the [SearchFragmentState] and applying [SearchFragmentAction]s.
@ -66,6 +71,51 @@ data class SearchFragmentState(
val searchAccessPoint: Event.PerformedSearch.SearchAccessPoint?
) : State
fun createInitialSearchFragmentState(
activity: HomeActivity,
components: Components,
tabId: String?,
pastedText: String?,
searchAccessPoint: Event.PerformedSearch.SearchAccessPoint
): SearchFragmentState {
val settings = components.settings
val tab = tabId?.let { components.core.store.state.findTab(it) }
val url = tab?.content?.url.orEmpty()
val currentSearchEngine = SearchEngineSource.Default(
components.search.provider.getDefaultEngine(activity)
)
val browsingMode = activity.browsingModeManager.mode
val areShortcutsAvailable = components.search.provider.areShortcutsAvailable(activity)
val shouldShowSearchSuggestions = when (browsingMode) {
BrowsingMode.Normal -> settings.shouldShowSearchSuggestions
BrowsingMode.Private ->
settings.shouldShowSearchSuggestions && settings.shouldShowSearchSuggestionsInPrivate
}
return SearchFragmentState(
query = url,
url = url,
searchTerms = tab?.content?.searchTerms.orEmpty(),
searchEngineSource = currentSearchEngine,
defaultEngineSource = currentSearchEngine,
showSearchSuggestions = shouldShowSearchSuggestions,
showSearchSuggestionsHint = false,
showSearchShortcuts = url.isEmpty() &&
areShortcutsAvailable &&
settings.shouldShowSearchShortcuts,
areShortcutsAvailable = areShortcutsAvailable,
showClipboardSuggestions = settings.shouldShowClipboardSuggestions,
showHistorySuggestions = settings.shouldShowHistorySuggestions,
showBookmarkSuggestions = settings.shouldShowBookmarkSuggestions,
tabId = tabId,
pastedText = pastedText,
searchAccessPoint = searchAccessPoint
)
}
/**
* Actions to dispatch through the `SearchStore` to modify `SearchState` through the reducer.
*/

@ -0,0 +1,17 @@
/* 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.search.ext
import android.content.Context
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
private const val MINIMUM_SEARCH_ENGINES_NUMBER_TO_SHOW_SHORTCUTS = 2
/**
* Return if the user has *at least 2* installed search engines.
* Useful to decide whether to show / enable certain functionalities.
*/
fun FenixSearchEngineProvider.areShortcutsAvailable(context: Context) =
installedSearchEngines(context).list.size >= MINIMUM_SEARCH_ENGINES_NUMBER_TO_SHOW_SHORTCUTS

@ -19,33 +19,22 @@ import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.fragment_search.view.*
import kotlinx.android.synthetic.main.fragment_search_dialog.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.selector.findTab
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.search.SearchEngineSource
import org.mozilla.fenix.search.SearchFragmentState
import org.mozilla.fenix.search.SearchFragmentStore
import org.mozilla.fenix.search.SearchInteractor
import org.mozilla.fenix.search.awesomebar.AwesomeBarView
import org.mozilla.fenix.search.createInitialSearchFragmentState
import org.mozilla.fenix.search.toolbar.ToolbarView
import org.mozilla.fenix.utils.Settings
typealias SearchDialogFragmentStore = SearchFragmentStore
typealias SearchDialogInteractor = SearchInteractor
fun Settings.shouldShowSearchSuggestions(isPrivate: Boolean): Boolean {
return if (isPrivate) {
shouldShowSearchSuggestions && shouldShowSearchSuggestionsInPrivate
} else {
shouldShowSearchSuggestions
}
}
class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
@ -72,8 +61,18 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val args by navArgs<SearchDialogFragmentArgs>()
val view = inflater.inflate(R.layout.fragment_search_dialog, container, false)
store = SearchDialogFragmentStore(setUpState())
store = SearchDialogFragmentStore(
createInitialSearchFragmentState(
activity as HomeActivity,
requireComponents,
tabId = args.sessionId,
pastedText = args.pastedText,
searchAccessPoint = args.searchAccessPoint
)
)
interactor = SearchDialogInteractor(
SearchDialogController(
@ -146,43 +145,4 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
return true
}
private fun setUpState(): SearchFragmentState {
val activity = activity as HomeActivity
val settings = activity.settings()
val args by navArgs<SearchDialogFragmentArgs>()
val tabId = args.sessionId
val tab = tabId?.let { requireComponents.core.store.state.findTab(it) }
val url = tab?.content?.url.orEmpty()
val currentSearchEngine = SearchEngineSource.Default(
requireComponents.search.provider.getDefaultEngine(requireContext())
)
val isPrivate = activity.browsingModeManager.mode.isPrivate
val areShortcutsAvailable =
requireContext().components.search.provider.installedSearchEngines(requireContext())
.list.size >= MINIMUM_SEARCH_ENGINES_NUMBER_TO_SHOW_SHORTCUTS
return SearchFragmentState(
query = url,
url = url,
searchTerms = tab?.content?.searchTerms.orEmpty(),
searchEngineSource = currentSearchEngine,
defaultEngineSource = currentSearchEngine,
showSearchSuggestions = settings.shouldShowSearchSuggestions(isPrivate),
showSearchSuggestionsHint = false,
showSearchShortcuts = settings.shouldShowSearchShortcuts &&
url.isEmpty() &&
areShortcutsAvailable,
areShortcutsAvailable = areShortcutsAvailable,
showClipboardSuggestions = settings.shouldShowClipboardSuggestions,
showHistorySuggestions = settings.shouldShowHistorySuggestions,
showBookmarkSuggestions = settings.shouldShowBookmarkSuggestions,
tabId = tabId,
pastedText = args.pastedText,
searchAccessPoint = args.searchAccessPoint
)
}
companion object {
private const val MINIMUM_SEARCH_ENGINES_NUMBER_TO_SHOW_SHORTCUTS = 2
}
}

@ -1,51 +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.session
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.ext.components
/**
* This observer starts and stops the service to show a notification
* indicating that a private tab is open.
*/
class NotificationSessionObserver(
private val applicationContext: Context,
private val notificationService: SessionNotificationService.Companion = SessionNotificationService
) {
private var scope: CoroutineScope? = null
@ExperimentalCoroutinesApi
fun start() {
scope = applicationContext.components.core.store.flowScoped { flow ->
flow.map { state -> state.privateTabs.isNotEmpty() }
.ifChanged()
.collect { hasPrivateTabs ->
if (hasPrivateTabs) {
notificationService.start(applicationContext, isStartedFromPrivateShortcut)
} else if (SessionNotificationService.started) {
notificationService.stop(applicationContext)
}
}
}
}
fun stop() {
scope?.cancel()
}
companion object {
var isStartedFromPrivateShortcut = false
}
}

@ -0,0 +1,66 @@
/* 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.session
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.privatemode.notification.AbstractPrivateNotificationService
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
/**
* Manages notifications for private tabs.
*
* Private tab notifications solve two problems for us:
* 1 - They allow users to interact with us from outside of the app (example: by closing all
* private tabs).
* 2 - The notification will keep our process alive, allowing us to keep private tabs in memory.
*
* As long as a session is active this service will keep its notification alive.
*/
class PrivateNotificationService : AbstractPrivateNotificationService() {
override val store: BrowserStore by lazy { components.core.store }
override fun NotificationCompat.Builder.buildNotification() {
setSmallIcon(R.drawable.ic_pbm_notification)
setContentTitle(getString(R.string.app_name_private_4, getString(R.string.app_name)))
setContentText(getString(R.string.notification_pbm_delete_text_2))
color = ContextCompat.getColor(this@PrivateNotificationService, R.color.pbm_notification_color)
}
override fun erasePrivateTabs() {
metrics.track(Event.PrivateBrowsingNotificationTapped)
val homeScreenIntent = Intent(this, HomeActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(HomeActivity.PRIVATE_BROWSING_MODE, isStartedFromPrivateShortcut)
}
if (VisibilityLifecycleCallback.finishAndRemoveTaskIfInBackground(this)) {
// Set start mode to be in background (recents screen)
homeScreenIntent.apply {
putExtra(HomeActivity.START_IN_RECENTS_SCREEN, true)
}
}
startActivity(homeScreenIntent)
super.erasePrivateTabs()
}
companion object {
/**
* Global used by [HomeActivity] to figure out if normal mode or private mode
* should be used after closing all private tabs.
*/
var isStartedFromPrivateShortcut = false
}
}

@ -1,174 +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.session
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import mozilla.components.browser.session.SessionManager
import mozilla.components.support.utils.ThreadUtils
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.sessionsOfType
/**
* Manages notifications for private tabs.
*
* Private tab notifications solve two problems for us:
* 1 - They allow users to interact with us from outside of the app (example: by closing all
* private tabs).
* 2 - The notification will keep our process alive, allowing us to keep private tabs in memory.
*
* As long as a session is active this service will keep its notification alive.
*/
class SessionNotificationService : Service() {
private var isStartedFromPrivateShortcut: Boolean = false
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val action = intent.action ?: return START_NOT_STICKY
when (action) {
ACTION_START -> {
isStartedFromPrivateShortcut = intent.getBooleanExtra(STARTED_FROM_PRIVATE_SHORTCUT, false)
createNotificationChannelIfNeeded()
startForeground(NOTIFICATION_ID, buildNotification())
}
ACTION_ERASE -> {
metrics.track(Event.PrivateBrowsingNotificationTapped)
val homeScreenIntent = Intent(this, HomeActivity::class.java)
val intentFlags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
homeScreenIntent.apply {
setFlags(intentFlags)
putExtra(HomeActivity.PRIVATE_BROWSING_MODE, isStartedFromPrivateShortcut)
}
if (VisibilityLifecycleCallback.finishAndRemoveTaskIfInBackground(this)) {
// Set start mode to be in background (recents screen)
homeScreenIntent.apply {
putExtra(HomeActivity.START_IN_RECENTS_SCREEN, true)
}
}
startActivity(homeScreenIntent)
components.core.sessionManager.removeAndCloseAllPrivateSessions()
}
else -> throw IllegalStateException("Unknown intent: $intent")
}
return START_NOT_STICKY
}
override fun onTaskRemoved(rootIntent: Intent) {
components.core.sessionManager.removeAndCloseAllPrivateSessions()
stopForeground(true)
stopSelf()
}
private fun buildNotification(): Notification {
return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setOngoing(true)
.setSmallIcon(R.drawable.ic_pbm_notification)
.setContentTitle(getString(R.string.app_name_private_4, getString(R.string.app_name)))
.setContentText(getString(R.string.notification_pbm_delete_text_2))
.setContentIntent(createNotificationIntent())
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
.setShowWhen(false)
.setLocalOnly(true)
.setColor(ContextCompat.getColor(this, R.color.pbm_notification_color))
.build()
}
private fun createNotificationIntent(): PendingIntent {
val intent = Intent(this, SessionNotificationService::class.java)
intent.action = ACTION_ERASE
return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_ONE_SHOT)
}
private fun createNotificationChannelIfNeeded() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// Notification channels are only available on Android O or higher.
return
}
val notificationManager = getSystemService<NotificationManager>() ?: return
val notificationChannelName = getString(R.string.notification_pbm_channel_name)
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID, notificationChannelName, NotificationManager.IMPORTANCE_MIN
)
channel.importance = NotificationManager.IMPORTANCE_LOW
channel.enableLights(false)
channel.enableVibration(false)
channel.setShowBadge(false)
notificationManager.createNotificationChannel(channel)
}
private fun SessionManager.removeAndCloseAllPrivateSessions() {
sessionsOfType(private = true).forEach { remove(it) }
}
override fun onBind(intent: Intent): IBinder? {
return null
}
companion object {
private const val NOTIFICATION_ID = 83
private const val NOTIFICATION_CHANNEL_ID = "browsing-session"
private const val STARTED_FROM_PRIVATE_SHORTCUT = "STARTED_FROM_PRIVATE_SHORTCUT"
private const val ACTION_START = "start"
private const val ACTION_ERASE = "erase"
internal var started = false
internal fun start(
context: Context,
startedFromPrivateShortcut: Boolean
) {
val intent = Intent(context, SessionNotificationService::class.java)
intent.action = ACTION_START
intent.putExtra(STARTED_FROM_PRIVATE_SHORTCUT, startedFromPrivateShortcut)
// From Focus #2901: The application is crashing due to the service not calling `startForeground`
// before it times out. This is a speculative fix to decrease the time between these two
// calls by running this after potentially expensive calls in FocusApplication.onCreate and
// BrowserFragment.inflateView by posting it to the end of the main thread.
ThreadUtils.postToMainThread(Runnable {
context.startService(intent)
})
started = true
}
internal fun stop(context: Context) {
val intent = Intent(context, SessionNotificationService::class.java)
// We want to make sure we always call stop after start. So we're
// putting these actions on the same sequential run queue.
ThreadUtils.postToMainThread(Runnable {
context.stopService(intent)
})
started = false
}
}
}

@ -302,7 +302,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
private fun setupPreferences() {
val leakKey = getPreferenceKey(R.string.pref_key_leakcanary)
val debuggingKey = getPreferenceKey(R.string.pref_key_remote_debugging)
val preferenceExternalDownloadManager = requirePreference<Preference>(R.string.pref_key_make_default_browser)
val preferenceExternalDownloadManager =
requirePreference<Preference>(R.string.pref_key_external_download_manager)
val preferenceLeakCanary = findPreference<Preference>(leakKey)
val preferenceRemoteDebugging = findPreference<Preference>(debuggingKey)
val preferenceMakeDefaultBrowser = requirePreference<Preference>(R.string.pref_key_make_default_browser)

@ -13,7 +13,6 @@ import android.view.ViewGroup
import androidx.core.content.pm.PackageInfoCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DividerItemDecoration
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
import kotlinx.android.synthetic.main.fragment_about.*
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.BuildConfig
@ -168,13 +167,8 @@ class AboutFragment : Fragment(), AboutPageListener {
}
private fun openLibrariesPage() {
startActivity(Intent(context, OssLicensesMenuActivity::class.java))
OssLicensesMenuActivity.setActivityTitle(
getString(
R.string.open_source_licenses_title,
appName
)
)
val intent = Intent(requireContext(), AboutLibrariesActivity::class.java)
startActivity(intent)
}
override fun onAboutItemClicked(item: AboutItem) {

@ -0,0 +1,120 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.settings.about
import android.graphics.Typeface
import android.os.Bundle
import android.text.util.Linkify
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import org.mozilla.fenix.R
import java.nio.charset.Charset
import java.util.Locale
/**
* Displays the licenses of all the libraries used by Fenix.
*
* This is a re-implementation of play-services-oss-licenses library.
* We can't use the official implementation in the OSS flavor of Fenix
* because it is proprietary and closed-source.
*
* There are popular FLOSS alternatives to Google's plugin and library
* such as AboutLibraries (https://github.com/mikepenz/AboutLibraries)
* but we considered the risk of introducing such third-party dependency
* to Fenix too high. Therefore, we use Google's gradle plugin to
* extract the dependencies and their licenses, and this activity
* to show the extracted licenses to the end-user.
*/
class AboutLibrariesActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appName = getString(R.string.app_name)
title = getString(R.string.open_source_licenses_title, appName)
setContentView(R.layout.about_libraries_activity)
setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
setupLibrariesListView()
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
private fun setupLibrariesListView() {
val libraries = parseLibraries()
val listView = findViewById<ListView>(R.id.about_libraries_listview)
listView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, libraries)
listView.setOnItemClickListener { _, _, position, _ ->
showLicenseDialog(libraries[position])
}
}
private fun parseLibraries(): List<LibraryItem> {
/*
The gradle plugin "oss-licenses-plugin" creates two "raw" resources:
- third_party_licenses which is the binary concatenation of all the licenses text for
all the libraries. License texts can either be an URL to a license file or just the
raw text of the license.
- third_party_licenses_metadata which contains one dependency per line formatted in
the following way: "[start_offset]:[length] [name]"
[start_offset] : first byte in third_party_licenses that contains the license
text for this library.
[length] : length of the license text for this library in
third_party_licenses.
[name] : either the name of the library, or its artifact name.
See https://github.com/google/play-services-plugins/tree/master/oss-licenses-plugin
*/
val licensesData = resources
.openRawResource(R.raw.third_party_licenses)
.readBytes()
val licensesMetadataReader = resources
.openRawResource(R.raw.third_party_license_metadata)
.bufferedReader()
return licensesMetadataReader.use { reader -> reader.readLines() }.map { line ->
val (section, name) = line.split(" ", limit = 2)
val (startOffset, length) = section.split(":", limit = 2).map(String::toInt)
val licenseData = licensesData.sliceArray(startOffset until startOffset + length)
val licenseText = licenseData.toString(Charset.forName("UTF-8"))
LibraryItem(name, licenseText)
}.sortedBy { item -> item.name.toLowerCase(Locale.ROOT) }
}
private fun showLicenseDialog(libraryItem: LibraryItem) {
val dialog = AlertDialog.Builder(this)
.setTitle(libraryItem.name)
.setMessage(libraryItem.license)
.create()
dialog.show()
val textView = dialog.findViewById<TextView>(android.R.id.message)!!
Linkify.addLinks(textView, Linkify.ALL)
textView.linksClickable = true
textView.textSize = LICENSE_TEXT_SIZE
textView.typeface = Typeface.MONOSPACE
}
companion object {
private const val LICENSE_TEXT_SIZE = 10F
}
}
private class LibraryItem(val name: String, val license: String) {
override fun toString(): String {
return name
}
}

@ -42,7 +42,7 @@ class CreateShortcutFragment : DialogFragment() {
cancel_button.setOnClickListener { dismiss() }
add_button.setOnClickListener {
val text = shortcut_text.text.toString()
viewLifecycleOwner.lifecycleScope.launch {
requireActivity().lifecycleScope.launch {
requireComponents.useCases.webAppUseCases.addToHomescreen(text)
}
dismiss()

@ -39,9 +39,14 @@ import mozilla.components.browser.tabstray.TabViewHolder
import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.toolbar.TabCounter.Companion.INFINITE_CHAR_PADDING_BOTTOM
import org.mozilla.fenix.components.toolbar.TabCounter.Companion.MAX_VISIBLE_TABS
import org.mozilla.fenix.components.toolbar.TabCounter.Companion.SO_MANY_TABS_OPEN
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.MultiselectModeChange
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentState.Mode
import java.text.NumberFormat
/**
* View that contains and configures the BrowserAwesomeBar
@ -71,7 +76,6 @@ class TabTrayView(
private val tabTrayItemMenu: TabTrayItemMenu
private var menu: BrowserMenu? = null
private val bottomSheetCallback: BottomSheetBehavior.BottomSheetCallback
private var tabsTouchHelper: TabsTouchHelper
private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate)
@ -85,9 +89,9 @@ class TabTrayView(
toggleFabText(isPrivate)
bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
if (!hasAccessibilityEnabled) {
if (interactor.onModeRequested() is Mode.Normal && !hasAccessibilityEnabled) {
if (slideOffset >= SLIDE_OFFSET) {
fabView.new_tab_button.show()
} else {
@ -102,9 +106,7 @@ class TabTrayView(
interactor.onTabTrayDismissed()
}
}
}
behavior.addBottomSheetCallback(bottomSheetCallback)
})
val selectedTabIndex = if (!isPrivate) {
DEFAULT_TAB_ID
@ -133,12 +135,14 @@ class TabTrayView(
setTopOffset(startingInLandscape)
val concatAdapter = ConcatAdapter(tabsAdapter)
view.tabsTray.apply {
layoutManager = LinearLayoutManager(container.context).apply {
reverseLayout = true
stackFromEnd = true
}
adapter = ConcatAdapter(collectionsButtonAdapter, tabsAdapter)
adapter = concatAdapter
tabsTouchHelper = TabsTouchHelper(
observable = tabsAdapter,
@ -149,6 +153,9 @@ class TabTrayView(
tabsAdapter.tabTrayInteractor = interactor
tabsAdapter.onTabsUpdated = {
// Put the 'Add to collections' button after the tabs have loaded.
concatAdapter.addAdapter(0, collectionsButtonAdapter)
if (hasAccessibilityEnabled) {
tabsAdapter.notifyDataSetChanged()
}
@ -266,17 +273,17 @@ class TabTrayView(
override fun onTabUnselected(tab: TabLayout.Tab?) { /*noop*/
}
var mode: TabTrayDialogFragmentState.Mode = TabTrayDialogFragmentState.Mode.Normal
var mode: Mode = Mode.Normal
private set
fun updateState(state: TabTrayDialogFragmentState) {
val oldMode = mode
if (oldMode::class != state.mode::class) {
updateTabsForMultiselectModeChanged(state.mode is TabTrayDialogFragmentState.Mode.MultiSelect)
updateTabsForMultiselectModeChanged(state.mode is Mode.MultiSelect)
if (view.context.settings().accessibilityServicesEnabled) {
view.announceForAccessibility(
if (state.mode == TabTrayDialogFragmentState.Mode.Normal) view.context.getString(
if (state.mode == Mode.Normal) view.context.getString(
R.string.tab_tray_exit_multiselect_content_description
) else view.context.getString(R.string.tab_tray_enter_multiselect_content_description)
)
@ -285,20 +292,18 @@ class TabTrayView(
mode = state.mode
when (state.mode) {
TabTrayDialogFragmentState.Mode.Normal -> {
Mode.Normal -> {
view.tabsTray.apply {
tabsTouchHelper.attachToRecyclerView(this)
}
behavior.addBottomSheetCallback(bottomSheetCallback)
toggleUIMultiselect(multiselect = false)
updateUINormalMode(state.browserState)
}
is TabTrayDialogFragmentState.Mode.MultiSelect -> {
is Mode.MultiSelect -> {
// Disable swipe to delete while in multiselect
tabsTouchHelper.attachToRecyclerView(null)
behavior.removeBottomSheetCallback(bottomSheetCallback)
toggleUIMultiselect(multiselect = true)
@ -372,7 +377,7 @@ class TabTrayView(
}
view.tab_tray_overflow.isVisible = !hasNoTabs
counter_text.text = "${browserState.normalTabs.size}"
counter_text.text = updateTabCounter(browserState.normalTabs.size)
updateTabCounterContentDescription(browserState.normalTabs.size)
adjustNewTabButtonsForNormalMode()
@ -457,6 +462,14 @@ class TabTrayView(
}
}
private fun updateTabCounter(count: Int): String {
if (count > MAX_VISIBLE_TABS) {
counter_text.setPadding(0, 0, 0, INFINITE_CHAR_PADDING_BOTTOM)
return SO_MANY_TABS_OPEN
}
return NumberFormat.getInstance().format(count.toLong())
}
fun setTopOffset(landscape: Boolean) {
val topOffset = if (landscape) {
0

@ -46,7 +46,9 @@ class ClearableEditText @JvmOverloads constructor(
* Displays a clear icon if text has been entered.
*/
override fun onTextChanged(text: CharSequence?, start: Int, lengthBefore: Int, lengthAfter: Int) {
val drawable = if (shouldShowClearButton(lengthAfter)) {
// lengthAfter has inconsistent behaviour when there are spaces in the entered text, so we'll use text.length.
val textLength = text?.length ?: 0
val drawable = if (shouldShowClearButton(textLength)) {
AppCompatResources.getDrawable(context, R.drawable.ic_clear)?.apply {
colorFilter = createBlendModeColorFilterCompat(context.getColorFromAttr(R.attr.primaryText), SRC_IN)
}

@ -0,0 +1,25 @@
<?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/. -->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/about_libraries"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context="org.mozilla.fenix.settings.about.AboutLibrariesActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar" />
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/about_libraries_listview" />
</RelativeLayout>

@ -10,12 +10,12 @@
android:minHeight="@dimen/radio_button_preference_height"
android:background="?android:selectableItemBackground"
android:clickable="true"
android:labelFor="@+id/radio_button"
android:focusable="true">
<RadioButton
android:id="@+id/radio_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:textAppearance="?android:attr/textAppearanceListItem"
android:layout_marginStart="@dimen/search_bar_search_engine_icon_padding"
android:layout_gravity="center" />

@ -25,6 +25,8 @@
<!-- Message announced to the user when tab tray is selected with 0 or 2+ tabs -->
<string name="open_tab_tray_plural">%1$s llingüetes abiertes. Toca pa cambiar a otra.</string>
<!-- Tab tray multi select title in app bar. The first parameter is the number of tabs selected -->
<string name="tab_tray_multi_select_title">%1$d esbillaes</string>
<!-- Label of editable text in create collection dialog for naming a new collection -->
<string name="tab_tray_add_new_collection_name">Nome</string>
@ -46,13 +48,15 @@
<!-- Text for the main message -->
<string name="cfr_message">¿Amestar un atayu p\'abrir llingüetes privaes dende la pantalla d\'aniciu?</string>
<!-- Text for the positive button -->
<string name="cfr_pos_button_text"></string>
<string name="cfr_pos_button_text">Amestalu</string>
<!-- Text for the negative button -->
<string name="cfr_neg_button_text">Non, gracies</string>
<!-- Search widget "contextual feature recommendation" (CFR) -->
<!-- Text for the main message. 'Firefox' intentionally hardcoded here.-->
<string name="search_widget_cfr_message">Accedi aína a Firefox col amiestu d\'un widget a la pantalla d\'aniciu.</string>
<!-- Text for the positive button -->
<string name="search_widget_cfr_pos_button_text">Amestalu</string>
<!-- Text for the negative button -->
<string name="search_widget_cfr_neg_button_text">Agora non</string>
@ -270,6 +274,8 @@
<string name="preferences_account_settings">Axustes de la cuenta</string>
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Abrir los enllaces nes aplicaciones</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Xestor de descargues esternu</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Complementos</string>
@ -349,6 +355,8 @@
<!-- Preference switch for crash reporter -->
<string name="preferences_crash_reporter">Informador de casques</string>
<!-- Preference switch for Mozilla location service -->
<string name="preferences_mozilla_location_service">Serviciu d\'allugamientu de Mozilla</string>
<!-- Preference switch for app health report. The first parameter is the name of the application (For example: Fenix) -->
<string name="preferences_fenix_health_report">Informe de salú de %s</string>
@ -470,6 +478,8 @@
<string name="history_delete_all">Desaniciar l\'historial</string>
<!-- Text for the dialog to confirm clearing all history -->
<string name="history_delete_all_dialog">¿De xuru que quies llimpiar l\'historial?</string>
<!-- Text for the snackbar to confirm that a single browsing history item has been deleted. The first parameter is the shortened URL of the deleted history item. -->
<string name="history_delete_single_item_snackbar">Desanicióse %1$s</string>
<!-- History overflow menu copy button -->
<string name="history_menu_copy_button">Copiar</string>
<!-- History overflow menu share button -->
@ -509,6 +519,10 @@
<!-- Confirmation message for a dialog confirming if the user wants to delete the selected folder -->
<string name="bookmark_delete_folder_confirmation_dialog">¿De xuru que quies desaniciar esta carpeta?</string>
<!-- Confirmation message for a dialog confirming if the user wants to delete multiple items including folders. Parameter will be replaced by app name. -->
<string name="bookmark_delete_multiple_folders_confirmation_dialog">%s va desaniciar los elementos esbillaos.</string>
<!-- Snackbar title shown after a folder has been deleted. This first parameter is the name of the deleted folder -->
<string name="bookmark_delete_folder_snackbar">Desanicióse %1$s</string>
<!-- Screen title for adding a bookmarks folder -->
<string name="bookmark_add_folder">Amestar una carpeta</string>
<!-- deprecated: Snackbar title shown after a bookmark has been created. -->
@ -550,6 +564,8 @@
<string name="bookmark_name_label">NOME</string>
<!-- Bookmark add folder screen title -->
<string name="bookmark_add_folder_fragment_label">Amiestu d\'una carpeta</string>
<!-- Bookmark select folder screen title -->
<string name="bookmark_select_folder_fragment_label">Esbilla d\'una carpeta</string>
<!-- Bookmark editing error missing title -->
<string name="bookmark_empty_title_error">Ha tener un títulu</string>
<!-- Bookmark editing error missing or improper URL -->
@ -561,6 +577,8 @@
<string name="bookmark_deletion_snackbar_message">Desanicióse %1$s</string>
<!-- Bookmark snackbar message on deleting multiple bookmarks not including folders-->
<string name="bookmark_deletion_multiple_snackbar_message_2">Desaniciáronse los marcadores</string>
<!-- Bookmark snackbar message on deleting multiple bookmarks including folders-->
<string name="bookmark_deletion_multiple_snackbar_message_3">Desaniciando les carpetes esbillaes</string>
<!-- Bookmark undo button for deletion snackbar action -->
<string name="bookmark_undo_deletion">DESFACER</string>
@ -1141,6 +1159,8 @@
<string name="preferences_passwords_exceptions_description_empty">Equí van amosase los anicios de sesión y contraseñes que nun se guarden</string>
<!-- Description of list of login exceptions that we never save logins for -->
<string name="preferences_passwords_exceptions_description">Los anicios de sesión y contraseñes d\'estos sitios nun van guardase.</string>
<!-- Text on button to remove all saved login exceptions -->
<string name="preferences_passwords_exceptions_remove_all">Desaniciar toles esceiciones</string>
<!-- Hint for search box in logins list -->
<string name="preferences_passwords_saved_logins_search">Guetar anicios de sesión</string>
<!-- Option to sort logins list A-Z, alphabetically -->

@ -303,6 +303,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Agor dolenni mewn apiau</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Rheolwr llwytho i lawr allanol</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Ychwanegion</string>

@ -309,6 +309,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Links in Apps öffnen</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Externer Download-Manager</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Add-ons</string>

@ -303,6 +303,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Wótkaze w nałoženjach wócyniś</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Eksterny zastojnik ześěgnjenjow</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Dodanki</string>

@ -147,8 +147,8 @@
<!-- Search Fragment -->
<!-- Button in the search view that lets a user search by scanning a QR code -->
<string name="search_scan_button">Σάρωση</string>
<!-- Button in the search view that lets a user search by using a shortcut -->
<string name="search_engines_shortcut_button">Μηχανή αναζήτησης</string>
<!-- Button in the search view that lets a user change their search engine -->
<string name="search_engine_button">Μηχανή αναζήτησης</string>
<!-- Button in the search view when shortcuts are displayed that takes a user to the search engine settings -->
<string name="search_shortcuts_engine_settings">Ρυθμίσεις μηχανής αναζήτησης</string>
<!-- Header displayed when selecting a shortcut search engine -->
@ -542,6 +542,8 @@
<string name="bookmark_select_folder">Επιλογή φακέλου</string>
<!-- Confirmation message for a dialog confirming if the user wants to delete the selected folder -->
<string name="bookmark_delete_folder_confirmation_dialog">Θέλετε σίγουρα να διαγράψετε αυτό το φάκελο;</string>
<!-- Confirmation message for a dialog confirming if the user wants to delete multiple items including folders. Parameter will be replaced by app name. -->
<string name="bookmark_delete_multiple_folders_confirmation_dialog">Το %s θα διαγράψει τα επιλεγμένα στοιχεία.</string>
<!-- Snackbar title shown after a folder has been deleted. This first parameter is the name of the deleted folder -->
<string name="bookmark_delete_folder_snackbar">Ο φάκελος &quot;%1$s&quot; διαγράφηκε</string>
<!-- Screen title for adding a bookmarks folder -->
@ -1011,6 +1013,11 @@
<!-- About page link text to open a screen with libraries that are used -->
<string name="about_other_open_source_libraries">Βιβλιοθήκες που χρησιμοποιούμε</string>
<!-- Toast shown to the user when they are activating the secret dev menu
The first parameter is number of long clicks left to enable the menu -->
<string name="about_debug_menu_toast_progress">Μενού εντοπισμού σφαλμάτων: απομένουν %1$d κλικ για ενεργοποίηση</string>
<string name="about_debug_menu_toast_done">Το μενού εντοπισμού σφαλμάτων ενεργοποιήθηκε</string>
<!-- Content description of the tab counter toolbar button when one tab is open -->
<string name="tab_counter_content_description_one_tab">1 καρτέλα</string>
<!-- Content description of the tab counter toolbar button when multiple tabs are open. First parameter will be replaced with the number of tabs (always more than one) -->
@ -1061,6 +1068,8 @@
<string name="preferences_passwords_sync_logins_sign_in">Σύνδεση στο Sync</string>
<!-- Preference to access list of saved logins -->
<string name="preferences_passwords_saved_logins">Αποθηκευμένες συνδέσεις</string>
<!-- Description of empty list of saved passwords. Placeholder is replaced with app name. -->
<string name="preferences_passwords_saved_logins_description_empty_text">Οι συνδέσεις που αποθηκεύετε ή συγχρονίζετε στο %s θα εμφανίζονται εδώ.</string>
<!-- Preference to access list of saved logins -->
<string name="preferences_passwords_saved_logins_description_empty_learn_more_link">Μάθετε περισσότερα σχετικά με το Sync.</string>
<!-- Preference to access list of login exceptions that we never save logins for -->
@ -1109,6 +1118,8 @@
<string name="logins_warning_dialog_later">Αργότερα</string>
<!-- Positive button to send users to set up a pin of warning dialog if users have no device authentication set up -->
<string name="logins_warning_dialog_set_up_now">Ρύθμιση τώρα</string>
<!-- Title of PIN verification dialog to direct users to re-enter their device credentials to access their logins -->
<string name="logins_biometric_prompt_message_pin">Ξεκλειδώστε τη συσκευή σας</string>
<!-- Saved logins sorting strategy menu item -by name- (if selected, it will sort saved logins alphabetically) -->
<string name="saved_logins_sort_strategy_alphabetically">Όνομα (Α-Ω)</string>
<!-- Saved logins sorting strategy menu item -by last used- (if selected, it will sort saved logins by last used) -->
@ -1150,6 +1161,9 @@
<!-- Text shown when we aren't able to validate the custom search query. The first parameter is the url of the custom search engine -->
<string name="search_add_custom_engine_error_cannot_reach">Σφάλμα σύνδεσης στο “%s”</string>
<!-- Text shown when a user creates a new search engine -->
<string name="search_add_custom_engine_success_message">Δημιουργήθηκε to %s</string>
<!-- Title text shown for the migration screen to the new browser. Placeholder replaced with app name -->
<string name="migration_title">Καλώς ορίσατε στο νέο %s</string>
<!-- Text on the disabled button while in progress. Placeholder replaced with app name -->
@ -1216,9 +1230,7 @@
<string name="saved_login_duplicate">Υπάρχει ήδη σύνδεση με αυτό το όνομα χρήστη</string>
<!-- Synced Tabs -->
<!-- Text displayed when user is not logged into a Firefox Account -->
<string name="synced_tabs_connect_to_sync_account">Συνδεθείτε με ένα λογαριασμό Firefox.</string>
<!-- 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>
@ -1231,13 +1243,4 @@
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">OK, το κατάλαβα</string>
<!-- DEPRECATED STRINGS -->
<!-- Button in the search view that lets a user search by using a shortcut -->
<string name="search_shortcuts_button">Συντομεύσεις</string>
<!-- DEPRECATED: Header displayed when selecting a shortcut search engine -->
<string name="search_shortcuts_search_with">Αναζήτηση με</string>
<!-- Header displayed when selecting a shortcut search engine -->
<string name="search_shortcuts_search_with_2">Αυτή τη φορά, αναζήτηση με:</string>
<!-- Preference title for switch preference to show search shortcuts -->
<string name="preferences_show_search_shortcuts">Εμφάνιση συντομεύσεων αναζήτησης</string>
</resources>

@ -301,6 +301,8 @@
<!-- 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 -->
<string name="preferences_external_download_manager">External download manager</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Add-ons</string>

@ -169,8 +169,8 @@
<!-- Search Fragment -->
<!-- Button in the search view that lets a user search by scanning a QR code -->
<string name="search_scan_button">Scan</string>
<!-- Button in the search view that lets a user search by using a shortcut -->
<string name="search_engines_shortcut_button">Search Engine</string>
<!-- Button in the search view that lets a user change their search engine -->
<string name="search_engine_button">Search engine</string>
<!-- Button in the search view when shortcuts are displayed that takes a user to the search engine settings -->
<string name="search_shortcuts_engine_settings">Search engine settings</string>
<!-- Header displayed when selecting a shortcut search engine -->
@ -301,6 +301,8 @@
<!-- 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 -->
<string name="preferences_external_download_manager">External download manager</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Add-ons</string>
@ -1448,9 +1450,7 @@
<string name="saved_login_duplicate">A login with that username already exists</string>
<!-- Synced Tabs -->
<!-- Text displayed when user is not logged into a Firefox Account -->
<string name="synced_tabs_connect_to_sync_account">Connect with a Firefox Account.</string>
<!-- 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>
@ -1471,13 +1471,4 @@
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">OK, Got It</string>
<!-- DEPRECATED STRINGS -->
<!-- Button in the search view that lets a user search by using a shortcut -->
<string name="search_shortcuts_button">Shortcuts</string>
<!-- DEPRECATED: Header displayed when selecting a shortcut search engine -->
<string name="search_shortcuts_search_with">Search with</string>
<!-- Header displayed when selecting a shortcut search engine -->
<string name="search_shortcuts_search_with_2">This time, search with:</string>
<!-- Preference title for switch preference to show search shortcuts -->
<string name="preferences_show_search_shortcuts">Show search shortcuts</string>
</resources>

@ -308,6 +308,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Abrir enlaces en aplicaciones</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Administrador de descargas externo</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Complementos</string>

@ -301,6 +301,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Abrir enlaces en aplicaciones</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Administrador de descargas externo</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Complementos</string>

@ -24,6 +24,16 @@
<!-- Message announced to the user when tab tray is selected with 0 or 2+ tabs -->
<string name="open_tab_tray_plural">%1$s زبانه‌های باز. برای تغییر زبانه‌ها ضربه بزنید.</string>
<!-- Tab tray multi select title in app bar. The first parameter is the number of tabs selected -->
<string name="tab_tray_multi_select_title">%1$d انتخاب شد</string>
<!-- Label of button in create collection dialog for creating a new collection -->
<string name="tab_tray_add_new_collection">افزودن مجموعه جدید</string>
<!-- Label of editable text in create collection dialog for naming a new collection -->
<string name="tab_tray_add_new_collection_name">نام</string>
<!-- Label of button in save to collection dialog for selecting a current collection -->
<string name="tab_tray_select_collection">انتخاب مجموعه</string>
<!-- Content description for checkmark while tab is selected while in multiselect mode in tab tray. The first parameter is the title of the tab selected -->
<string name="tab_tray_item_selected_multiselect_content_description">%1$s انتخاب شد</string>
<!-- Content description on checkmark while tab is selected in multiselect mode in tab tray -->
<string name="tab_tray_multiselect_selected_content_description">انتخاب شده</string>
@ -577,6 +587,8 @@
<string name="bookmark_select_folder">انتخاب پوشه</string>
<!-- Confirmation message for a dialog confirming if the user wants to delete the selected folder -->
<string name="bookmark_delete_folder_confirmation_dialog">آيا از حذف اين پوشه مطمئن هستيد؟</string>
<!-- Confirmation message for a dialog confirming if the user wants to delete multiple items including folders. Parameter will be replaced by app name. -->
<string name="bookmark_delete_multiple_folders_confirmation_dialog">%s موارد انتخاب شده را حذف می کند.</string>
<!-- Snackbar title shown after a folder has been deleted. This first parameter is the name of the deleted folder -->
<string name="bookmark_delete_folder_snackbar">%1$s حذف شد</string>
<!-- Screen title for adding a bookmarks folder -->

@ -308,6 +308,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Avaa linkit sovelluksissa</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Erillinen latausten hallinta</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Lisäosat</string>

@ -306,6 +306,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Ouvrir les liens dans des applications</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Gestionnaire de téléchargement externe</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Modules complémentaires</string>

@ -306,6 +306,8 @@
<string name="preferences_account_settings">Mbaete ñemboheko</string>
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Embojuruja juajuha tembipuruípe</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Mboguejy okaygua ñangarekoha</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Moĩmbaha</string>

@ -302,6 +302,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Otvori poveznice u aplikacijama</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Vanjski upravitelj preuzimanja</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Dodaci</string>

@ -303,6 +303,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Wotkazy w nałoženjach wočinić</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Eksterny zrjadowak sćehnjenjow</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Přidatki</string>

@ -305,6 +305,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Hivatkozások megnyitása alkalmazásokban</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Külső letöltéskezelő</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Kiegészítők</string>

@ -311,6 +311,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Buka tautan di aplikasi</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Pengelola unduhan eksternal</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Pengaya</string>

@ -309,6 +309,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Apri collegamenti nelle app</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Gestore download esterno</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Componenti aggiuntivi</string>

@ -299,6 +299,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">פתיחת קישורים ביישומונים</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">מנהל הורדות חיצוני</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">תוספות</string>
@ -1445,6 +1447,9 @@
<!-- Text displayed in the synced tabs screen when a user is not signed in to Firefox Sync describing Synced Tabs -->
<string name="synced_tabs_sign_in_message">הצגת רשימת הלשוניות מהמכשירים האחרים שלך.</string>
<!-- Text displayed on a button in the synced tabs screen to link users to sign in when a user is not signed in to Firefox Sync -->
<string name="synced_tabs_sign_in_button">יש להתחבר כדי לסנכרן</string>
<!-- Top Sites -->
<!-- Title text displayed in the dialog when top sites limit is reached. -->
<string name="top_sites_max_limit_title">הגעת למכסת האתרים המובילים</string>

@ -301,6 +301,8 @@
<string name="preferences_account_settings">ანგარიშის პარამეტრები</string>
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">ბმულების გახსნა პროგრამებში</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">ჩამოტვირთვის გარეშე მმართველი</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">დამატებები</string>

@ -306,6 +306,8 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Ldi iseɣwan deg isnasen</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Amsefrak n usider azɣaray</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Izegrar</string>

@ -297,6 +297,8 @@
<string name="preferences_account_settings">Тіркелгі баптаулары</string>
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Сілтемелерді қолданбаларда ашу</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Сыртқы жүктеу менеджері</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Қосымшалар</string>

@ -30,16 +30,16 @@
<!-- Tab tray multi select title in app bar. The first parameter is the number of tabs selected -->
<string name="tab_tray_multi_select_title">%1$d개 선택됨</string>
<!-- Label of button in create collection dialog for creating a new collection -->
<string name="tab_tray_add_new_collection">컬렉션 추가</string>
<string name="tab_tray_add_new_collection">모음집 추가</string>
<!-- Label of editable text in create collection dialog for naming a new collection -->
<string name="tab_tray_add_new_collection_name">이름</string>
<!-- Label of button in save to collection dialog for selecting a current collection -->
<string name="tab_tray_select_collection">컬렉션 선택</string>
<string name="tab_tray_select_collection">모음집 선택</string>
<!-- Content description for close button while in multiselect mode in tab tray -->
<string name="tab_tray_close_multiselect_content_description">다중 선택 모드 종료</string>
<!-- Content description for save to collection button while in multiselect mode in tab tray -->
<string name="tab_tray_collection_button_multiselect_content_description">선택한 탭을 컬렉션에 저장</string>
<string name="tab_tray_collection_button_multiselect_content_description">선택한 탭을 모음집에 저장</string>
<!-- Content description for checkmark while tab is selected while in multiselect mode in tab tray. The first parameter is the title of the tab selected -->
<string name="tab_tray_item_selected_multiselect_content_description">%1$s 선택됨</string>
@ -49,7 +49,7 @@
<!-- Content description announcement when exiting multiselect mode in tab tray -->
<string name="tab_tray_exit_multiselect_content_description">다중 선택 모드 종료됨</string>
<!-- Content description announcement when entering multiselect mode in tab tray -->
<string name="tab_tray_enter_multiselect_content_description">다중 선택 모드로 전환됨, 컬렉션에 저장할 탭을 선택하세요</string>
<string name="tab_tray_enter_multiselect_content_description">다중 선택 모드로 전환됨, 모음집에 저장할 탭을 선택하세요</string>
<!-- Content description on checkmark while tab is selected in multiselect mode in tab tray -->
<string name="tab_tray_multiselect_selected_content_description">선택됨</string>
@ -137,7 +137,7 @@
<!-- Browser menu button that creates a new tab -->
<string name="browser_menu_new_tab">새 탭</string>
<!-- Browser menu button that saves the current tab to a collection -->
<string name="browser_menu_save_to_collection_2">컬렉션에 저장</string>
<string name="browser_menu_save_to_collection_2">모음집에 저장</string>
<!-- Browser menu button that open a share menu to share the current site -->
<string name="browser_menu_share">공유</string>
<!-- Share menu title, displayed when a user is sharing their current site -->
@ -314,6 +314,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">앱에서 링크 열기</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">외부 다운로드 관리자</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">부가 기능</string>
@ -495,7 +497,7 @@
<!-- Text shown as the title of the open tab tray -->
<string name="tab_tray_title">탭 열기</string>
<!-- Text shown in the menu for saving tabs to a collection -->
<string name="tab_tray_menu_item_save">컬렉션에 저장</string>
<string name="tab_tray_menu_item_save">모음집에 저장</string>
<!-- Text shown in the menu for sharing all tabs -->
<string name="tab_tray_menu_item_share">모든 탭 공유</string>
<!-- Text shown in the menu for closing all tabs -->
@ -507,7 +509,7 @@
<!-- Shortcut action to toggle private mode -->
<string name="tab_tray_menu_toggle">탭 모드 전환</string>
<!-- Content description (not visible, for screen readers etc.): Removes tab from collection button. Removes the selected tab from collection when pressed -->
<string name="remove_tab_from_collection">컬렉션에서 탭 삭제</string>
<string name="remove_tab_from_collection">모음집에서 탭 삭제</string>
<!-- Content description (not visible, for screen readers etc.): Close tab button. Closes the current session when pressed -->
<string name="close_tab">탭 닫기</string>
<!-- Content description (not visible, for screen readers etc.): Close tab <title> button. First parameter is tab title -->
@ -519,7 +521,7 @@
<!-- Open tabs menu item to share all tabs -->
<string name="tabs_menu_share_tabs">탭 공유</string>
<!-- Open tabs menu item to save tabs to collection -->
<string name="tabs_menu_save_to_collection1">탭을 컬렉션에 저장</string>
<string name="tabs_menu_save_to_collection1">탭을 모음집에 저장</string>
<!-- Content description (not visible, for screen readers etc.): Opens the tab menu when pressed -->
<string name="tab_menu">탭 메뉴</string>
<!-- Tab menu item to share the tab -->
@ -534,11 +536,11 @@
<string name="current_session_image">현재 세션 이미지</string>
<!-- Button to save the current set of tabs into a collection -->
<string name="save_to_collection">컬렉션에 저장</string>
<string name="save_to_collection">모음집에 저장</string>
<!-- Text for the menu button to delete a collection -->
<string name="collection_delete">컬렉션 삭제</string>
<string name="collection_delete">모음집 삭제</string>
<!-- Text for the menu button to rename a collection -->
<string name="collection_rename">컬렉션 이름 변경</string>
<string name="collection_rename">모음집 이름 변경</string>
<!-- Text for the button to open tabs of the selected collection -->
<string name="collection_open_tabs">열린 탭</string>
@ -749,25 +751,25 @@
<!-- Collections -->
<!-- Collections header on home fragment -->
<string name="collections_header">컬렉션</string>
<string name="collections_header">모음집</string>
<!-- Content description (not visible, for screen readers etc.): Opens the collection menu when pressed -->
<string name="collection_menu_button_content_description">컬렉션 메뉴</string>
<string name="collection_menu_button_content_description">모음집 메뉴</string>
<!-- No Open Tabs Message Header -->
<string name="no_collections_header1">중요한 것들을 모으세요</string>
<string name="no_collections_header1">중요한 것들 수집하기</string>
<!-- Label to describe what collections are to a new user without any collections -->
<string name="no_collections_description1">나중에 빠르게 액세스 할 수 있도록 유사한 검색, 사이트 및 탭을 그룹화하세요.</string>
<string name="no_collections_description1">나중에 빠르게 접근할 수 있도록 유사한 검색, 사이트 및 탭을 모아 보세요.</string>
<!-- Title for the "select tabs" step of the collection creator -->
<string name="create_collection_select_tabs">탭 선택</string>
<!-- Title for the "select collection" step of the collection creator -->
<string name="create_collection_select_collection">컬렉션 선택</string>
<string name="create_collection_select_collection">모음집 선택</string>
<!-- Title for the "name collection" step of the collection creator -->
<string name="create_collection_name_collection">컬렉션 이름</string>
<string name="create_collection_name_collection">모음집 이름</string>
<!-- Button to add new collection for the "select collection" step of the collection creator -->
<string name="create_collection_add_new_collection">컬렉션 추가</string>
<string name="create_collection_add_new_collection">모음집 추가</string>
<!-- Button to select all tabs in the "select tabs" step of the collection creator -->
<string name="create_collection_select_all">모두 선택</string>
@ -788,7 +790,7 @@
<string name="create_collection_tabs_saved">탭이 저장되었습니다!</string>
<!-- Text shown in snackbar when one or multiple tabs have been saved in a new collection -->
<string name="create_collection_tabs_saved_new_collection">컬렉션 저장됨!</string>
<string name="create_collection_tabs_saved_new_collection">모음집 저장됨!</string>
<!-- Text shown in snackbar when one tab has been saved in a collection -->
<string name="create_collection_tab_saved">탭이 저장되었습니다!</string>
@ -802,7 +804,7 @@
<string name="create_collection_view">보기</string>
<!-- Default name for a new collection in "name new collection" step of the collection creator. %d is a placeholder for the number of collections-->
<string name="create_collection_default_name">컬렉션 %d개</string>
<string name="create_collection_default_name">모음집 %d개</string>
<!-- Share -->
<!-- Share screen header -->
@ -862,10 +864,10 @@
<!-- Name of the "Powered by Fenix" notification channel. Displayed in the "App notifications" system settings for the app -->
<string name="notification_powered_by_channel_name">제공:</string>
<!-- Text shown in snackbar when user deletes a collection -->
<string name="snackbar_collection_deleted">컬렉션 삭제됨</string>
<string name="snackbar_collection_deleted">모음집 삭제됨</string>
<!-- Text shown in snackbar when user renames a collection -->
<string name="snackbar_collection_renamed">컬렉션 이름 변경됨</string>
<string name="snackbar_collection_renamed">모음집 이름 변경됨</string>
<!-- Text shown in snackbar when user deletes a tab -->
<string name="snackbar_tab_deleted">탭 삭제됨</string>
@ -902,9 +904,9 @@
<!-- Tab collection deletion prompt dialog message. Placeholder will be replaced with the collection name -->
<string name="tab_collection_dialog_message">%1$s 파일을 삭제하시겠습니까?</string>
<!-- Collection and tab deletion prompt dialog message. This will show when the last tab from a collection is deleted -->
<string name="delete_tab_and_collection_dialog_message">이 탭을 삭제하면 전체 컬렉션이 삭제됩니다. 언제든지 새 컬렉션을 만들 수 있습니다.</string>
<string name="delete_tab_and_collection_dialog_message">이 탭을 삭제하면 전체 모음집이 삭제됩니다. 언제든지 새 모음집을 만들 수 있습니다.</string>
<!-- Collection and tab deletion prompt dialog title. Placeholder will be replaced with the collection name. This will show when the last tab from a collection is deleted -->
<string name="delete_tab_and_collection_dialog_title">%1$s 컬렉션을 삭제하시겠습니까?</string>
<string name="delete_tab_and_collection_dialog_title">%1$s 모음집을 삭제하시겠습니까?</string>
<!-- Tab collection deletion prompt dialog option to delete the collection -->
<string name="tab_collection_dialog_positive">삭제</string>
<!-- Tab collection deletion prompt dialog option to cancel deleting the collection -->
@ -1184,9 +1186,9 @@
<!-- Option for enhanced tracking protection for the custom protection settings for tracking content-->
<string name="preference_enhanced_tracking_protection_custom_tracking_content_3">사용자 지정 탭에서만</string>
<!-- Preference for enhanced tracking protection for the custom protection settings -->
<string name="preference_enhanced_tracking_protection_custom_cryptominers">크립토마이너</string>
<string name="preference_enhanced_tracking_protection_custom_cryptominers">암호화폐 채굴기</string>
<!-- Preference for enhanced tracking protection for the custom protection settings -->
<string name="preference_enhanced_tracking_protection_custom_fingerprinters">핑거프린터</string>
<string name="preference_enhanced_tracking_protection_custom_fingerprinters">디지털 지문</string>
<string name="enhanced_tracking_protection_blocked">차단됨</string>
<!-- Header for categories that are being not being blocked by current Enhanced Tracking Protection settings -->
<string name="enhanced_tracking_protection_allowed">허용됨</string>
@ -1199,11 +1201,11 @@
<!-- Description of cross-site tracking cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_cookies_description">광고 네트워크 및 분석 회사가 여러 사이트에서 사용자 탐색 데이터를 컴파일하는데 사용하는 쿠키를 차단합니다.</string>
<!-- Category of trackers (cryptominers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_cryptominers_title">크립토마이너</string>
<string name="etp_cryptominers_title">암호화폐 채굴기</string>
<!-- Description of cryptominers that can be blocked by Enhanced Tracking Protection -->
<string name="etp_cryptominers_description">악의적인 스크립트가 디지털 통화를 채굴하기 위해 기기에 액세스하는 것을 방지합니다.</string>
<!-- Category of trackers (fingerprinters) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_fingerprinters_title">핑거프린터</string>
<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 -->

@ -25,6 +25,29 @@
<!-- Message announced to the user when tab tray is selected with 0 or 2+ tabs -->
<string name="open_tab_tray_plural">%1$s atvertos kortelės. Bakstelėkite, norėdami pereiti tarp kortelių.</string>
<!-- Tab tray multi select title in app bar. The first parameter is the number of tabs selected -->
<string name="tab_tray_multi_select_title">Pasirinkta %1$d</string>
<!-- Label of button in create collection dialog for creating a new collection -->
<string name="tab_tray_add_new_collection">Pridėti naują rinkinį</string>
<!-- Label of editable text in create collection dialog for naming a new collection -->
<string name="tab_tray_add_new_collection_name">Pavadinimas</string>
<!-- Label of button in save to collection dialog for selecting a current collection -->
<string name="tab_tray_select_collection">Pasirinkite rinkinį</string>
<!-- Content description for close button while in multiselect mode in tab tray -->
<string name="tab_tray_close_multiselect_content_description">Išeiti iš daugelio pasirinkimų veiksenos</string>
<!-- Content description for save to collection button while in multiselect mode in tab tray -->
<string name="tab_tray_collection_button_multiselect_content_description">Įtraukti pažymėtas korteles į rinkinį</string>
<!-- Content description for checkmark while tab is selected while in multiselect mode in tab tray. The first parameter is the title of the tab selected -->
<string name="tab_tray_item_selected_multiselect_content_description">Pažymėta %1$s</string>
<!-- Content description when tab is unselected while in multiselect mode in tab tray. The first parameter is the title of the tab unselected -->
<string name="tab_tray_item_unselected_multiselect_content_description">Atžymėta %1$s</string>
<!-- Content description announcement when exiting multiselect mode in tab tray -->
<string name="tab_tray_exit_multiselect_content_description">Išeita iš daugelio pasirinkimų veiksenos</string>
<!-- Content description announcement when entering multiselect mode in tab tray -->
<string name="tab_tray_enter_multiselect_content_description">Įjungta daugelio pasirinkimų veiksena, pažymėtos kortelės bus įtrauktos į rinkinį</string>
<!-- Content description on checkmark while tab is selected in multiselect mode in tab tray -->
<string name="tab_tray_multiselect_selected_content_description">Pažymėta</string>
<!-- About content. The first parameter is the name of the application. (For example: Fenix) -->
<string name="about_content">„%1$s“ kūrėjai yra „Adam Novak“.</string>
@ -147,10 +170,12 @@
<!-- Search Fragment -->
<!-- Button in the search view that lets a user search by scanning a QR code -->
<string name="search_scan_button">Nuskaityti</string>
<!-- Button in the search view that lets a user search by using a shortcut -->
<string name="search_engines_shortcut_button">Ieškyklė</string>
<!-- Button in the search view that lets a user change their search engine -->
<string name="search_engine_button">Ieškyklė</string>
<!-- Button in the search view when shortcuts are displayed that takes a user to the search engine settings -->
<string name="search_shortcuts_engine_settings">Ieškyklės nuostatos</string>
<!-- Header displayed when selecting a shortcut search engine -->
<string name="search_engines_search_with">Šįkart ieškoti su:</string>
<!-- Button in the search view that lets a user navigate to the site in their clipboard -->
<string name="awesomebar_clipboard_title">Atverti saitą iš iškarpinės</string>
<!-- Button in the search suggestions onboarding that allows search suggestions in private sessions -->
@ -276,6 +301,8 @@
<string name="preferences_account_settings">Paskyros nuostatos</string>
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Atverti saitus programose</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Išorinė atsiuntimų tvarkytuvė</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Priedai</string>
@ -509,6 +536,9 @@
<!-- Postfix for private WebApp titles, placeholder is replaced with app name -->
<string name="pwa_site_controls_title_private">„%1$s“ (privačioji veiksena)</string>
<!-- Button in the current tab tray header in multiselect mode. Saved the selected tabs to a collection when pressed. -->
<string name="tab_tray_save_to_collection">Įrašyti</string>
<!-- History -->
<!-- Text for the button to clear all history -->
<string name="history_delete_all">Išvalyti žurnalą</string>
@ -1245,6 +1275,8 @@
<!-- Description of list of login exceptions that we never save logins for -->
<string name="preferences_passwords_exceptions_description">Šių svetainių prisijungimai ir slaptažodžiai nebus įrašomi.</string>
<!-- Text on button to remove all saved login exceptions -->
<string name="preferences_passwords_exceptions_remove_all">Pašalinti visas išimtis</string>
<!-- Hint for search box in logins list -->
<string name="preferences_passwords_saved_logins_search">Ieškoti prisijungimų</string>
<!-- Option to sort logins list A-Z, alphabetically -->
@ -1441,9 +1473,7 @@
<string name="saved_login_duplicate">Prisijungimas su tokiu naudotojo vardu jau yra</string>
<!-- Synced Tabs -->
<!-- Text displayed when user is not logged into a Firefox Account -->
<string name="synced_tabs_connect_to_sync_account">Prisijunkite su „Firefox“ paskyra.</string>
<!-- 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">Susiekite kitą įrenginį.</string>
<!-- Text displayed asking user to re-authenticate -->
<string name="synced_tabs_reauth">Prisijunkite iš naujo.</string>
@ -1465,15 +1495,4 @@
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">Gerai, supratau</string>
<!-- DEPRECATED STRINGS -->
<!-- Button in the search view that lets a user search by using a shortcut -->
<string name="search_shortcuts_button">Leistukai</string>
<!-- DEPRECATED: Header displayed when selecting a shortcut search engine -->
<string name="search_shortcuts_search_with">Ieškoti per</string>
<!-- Header displayed when selecting a shortcut search engine -->
<string name="search_shortcuts_search_with_2">Šįkart ieškoti su:</string>
<!-- Preference title for switch preference to show search shortcuts -->
<string name="preferences_show_search_shortcuts">Rodyti paieškos leistukus</string>
</resources>

@ -305,6 +305,8 @@
<string name="preferences_account_settings">Kontoinnstillinger</string>
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Åpne lenker i apper</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Ekstern nedlastingsbehandler</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Utvidelser</string>

@ -40,6 +40,8 @@
<string name="tab_tray_collection_button_multiselect_content_description">Geselecteerde tabbladen in collectie opslaan</string>
<!-- Content description for checkmark while tab is selected while in multiselect mode in tab tray. The first parameter is the title of the tab selected -->
<string name="tab_tray_item_selected_multiselect_content_description">%1$s geselecteerd</string>
<!-- Content description when tab is unselected while in multiselect mode in tab tray. The first parameter is the title of the tab unselected -->
<string name="tab_tray_item_unselected_multiselect_content_description">Selectie %1$s ongedaan gemaakt</string>
<!-- Content description on checkmark while tab is selected in multiselect mode in tab tray -->
<string name="tab_tray_multiselect_selected_content_description">Geselecteerd</string>
@ -302,6 +304,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Koppelingen openen in apps</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Externe downloadbeheerder</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Add-ons</string>

@ -306,6 +306,8 @@
<string name="preferences_account_settings">Kontoinnstillingar</string>
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Opne lenker i appar</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Ekstern nedlastingshandterar</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Tillegg</string>

@ -300,6 +300,8 @@
<string name="preferences_account_settings">Paramètres del compte</string>
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Dobrir los ligams dins las aplicacions</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Gestionari de telecargament extèrn</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Moduls complementaris</string>
@ -992,6 +994,11 @@
In English this is an idiom for "choose a side as in an argument or fight"
but it is ok to make this more literally about "choosing a position in a physical space -->
<string name="onboarding_toolbar_position_header">Prenètz posicion</string>
<!-- text for the private browsing onboarding card header -->
<string name="onboarding_private_browsing_header">Navegatz dun biais privat</string>
<!-- text for the private browsing onboarding card description
The first parameter is an icon that represents private browsing -->
<string name="onboarding_private_browsing_description1">Dobrir un sol onglet privat : tocatz licòna %s.</string>
<!-- text for the private browsing onbording card button, that launches settings -->
<string name="onboarding_private_browsing_button">Dobrir los paramètres</string>
<!-- text for the privacy notice onboarding card header -->

@ -175,8 +175,8 @@
<!-- Search Fragment -->
<!-- Button in the search view that lets a user search by scanning a QR code -->
<string name="search_scan_button">ਸਕੈਨ ਕਰੋ</string>
<!-- Button in the search view that lets a user search by using a shortcut -->
<string name="search_engines_shortcut_button">ਖੋਜ ਇੰਜਣ</string>
<!-- Button in the search view that lets a user change their search engine -->
<string name="search_engine_button">ਖੋਜ ਇੰਜਣ</string>
<!-- Button in the search view when shortcuts are displayed that takes a user to the search engine settings -->
<string name="search_shortcuts_engine_settings">ਖੋਜ ਇੰਜਣ ਸੈਟਿੰਗਾਂ</string>
<!-- Header displayed when selecting a shortcut search engine -->
@ -308,6 +308,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">ਲਿੰਕ ਐਪਾਂ ਵਿੱਚ ਖੋਲ੍ਹੋ</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">ਬਾਹਰੀ ਡਾਊਨਲੋਡ ਮੈਨੇਜਰ</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">ਐਡ-ਆਨ</string>
@ -1471,9 +1473,7 @@
<string name="saved_login_duplicate">ਉਸ ਵਰਤੋਂਕਾਰ ਨਾਲ ਲਾਗਇਨ ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ</string>
<!-- Synced Tabs -->
<!-- Text displayed when user is not logged into a Firefox Account -->
<string name="synced_tabs_connect_to_sync_account">Firefox Account ਨਾਲ ਕਨੈਕਟ ਕਰੋ।</string>
<!-- 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>
@ -1494,13 +1494,4 @@
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">ਠੀਕ ਹੈ, ਸਮਝ ਗਏ</string>
<!-- DEPRECATED STRINGS -->
<!-- Button in the search view that lets a user search by using a shortcut -->
<string name="search_shortcuts_button">ਸ਼ਾਰਟਕੱਟ</string>
<!-- DEPRECATED: Header displayed when selecting a shortcut search engine -->
<string name="search_shortcuts_search_with">ਇਸ ਨਾਲ ਖੋਜੋ</string>
<!-- Header displayed when selecting a shortcut search engine -->
<string name="search_shortcuts_search_with_2">ਇਸ ਵੇਲੇ, ਇਸ ਨਾਲ ਖੋਜੋ:</string>
<!-- Preference title for switch preference to show search shortcuts -->
<string name="preferences_show_search_shortcuts">ਖੋਜ ਸ਼ਾਰਟਕੱਟ ਵੇਖਾਓ</string>
</resources>

@ -306,6 +306,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Otwieranie odnośników w aplikacjach</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Zewnętrzny menedżer pobierania</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Dodatki</string>

@ -302,6 +302,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Abrir links em aplicativos</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Gerenciador de downloads externo</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Extensões</string>
@ -539,7 +541,7 @@
<!-- Text for the button to clear all history -->
<string name="history_delete_all">Limpar histórico</string>
<!-- Text for the dialog to confirm clearing all history -->
<string name="history_delete_all_dialog">Tem certeza que deseja limpar seu histórico?</string>
<string name="history_delete_all_dialog">Tem certeza que quer limpar o histórico?</string>
<!-- Text for the snackbar to confirm that multiple browsing history items has been deleted -->
<string name="history_delete_multiple_items_snackbar">Histórico excluído</string>
<!-- Text for the snackbar to confirm that a single browsing history item has been deleted. The first parameter is the shortened URL of the deleted history item. -->
@ -599,7 +601,7 @@
<!-- Screen title for selecting a bookmarks folder -->
<string name="bookmark_select_folder">Selecionar pasta</string>
<!-- Confirmation message for a dialog confirming if the user wants to delete the selected folder -->
<string name="bookmark_delete_folder_confirmation_dialog">Tem certeza que deseja excluir esta pasta?</string>
<string name="bookmark_delete_folder_confirmation_dialog">Tem certeza que quer excluir esta pasta?</string>
<!-- Confirmation message for a dialog confirming if the user wants to delete multiple items including folders. Parameter will be replaced by app name. -->
<string name="bookmark_delete_multiple_folders_confirmation_dialog">O %s excluirá os itens selecionados.</string>
<!-- Snackbar title shown after a folder has been deleted. This first parameter is the name of the deleted folder -->
@ -859,7 +861,7 @@
<!-- QR code scanner prompt dialog positive option to deny navigation to scanned link -->
<string name="qr_scanner_dialog_negative">NEGAR</string>
<!-- Tab collection deletion prompt dialog message. Placeholder will be replaced with the collection name -->
<string name="tab_collection_dialog_message">Tem certeza que deseja excluir %1$s?</string>
<string name="tab_collection_dialog_message">Tem certeza que quer excluir %1$s?</string>
<!-- Collection and tab deletion prompt dialog message. This will show when the last tab from a collection is deleted -->
<string name="delete_tab_and_collection_dialog_message">Excluir esta aba também excluirá toda a coleção. Você pode criar novas coleções quando quiser.</string>
<!-- Collection and tab deletion prompt dialog title. Placeholder will be replaced with the collection name. This will show when the last tab from a collection is deleted -->
@ -985,7 +987,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. Deseja entrar com esta conta?</string>
<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>
<!-- 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 -->
@ -1006,26 +1008,26 @@
<!-- text for tracking protection radio button option for standard level of blocking -->
<string name="onboarding_tracking_protection_standard_button_2">Padrão (predefinido)</string>
<!-- text for standard blocking option button description -->
<string name="onboarding_tracking_protection_standard_button_description_2">Bloqueia menos rastreadores. Páginas são carregadas normalmente.</string>
<string name="onboarding_tracking_protection_standard_button_description_2">Bloqueia menos rastreadores. As páginas são carregadas normalmente.</string>
<!-- text for tracking protection radio button option for strict level of blocking -->
<string name="onboarding_tracking_protection_strict_button">Rigoroso (recomendado)</string>
<!-- text for tracking protection radio button option for strict level of blocking -->
<string name="onboarding_tracking_protection_strict_option">Rigoroso</string>
<!-- text for strict blocking option button description -->
<string name="onboarding_tracking_protection_strict_button_description_2">Bloqueia mais rastreadores, anúncios e notificações. Páginas são carregadas mais rápido, mas algumas coisas podem não funcionar.</string>
<string name="onboarding_tracking_protection_strict_button_description_2">Bloqueia mais rastreadores, anúncios e notificações. As páginas são carregadas mais rápido, mas algumas coisas podem não funcionar.</string>
<!-- text for the toolbar position card header
In English this is an idiom for "choose a side as in an argument or fight"
but it is ok to make this more literally about "choosing a position in a physical space -->
<string name="onboarding_toolbar_position_header">Escolha uma posição</string>
<!-- text for the toolbar position card description -->
<string name="onboarding_toolbar_position_description">Experimente navegar com apenas uma mão, usando a barra de ferramentas embaixo, ou a mova para o alto.</string>
<string name="onboarding_toolbar_position_description">Experimente navegar usando apenas uma mão com a barra de ferramentas embaixo, ou a mova para o alto.</string>
<!-- text for the private browsing onboarding card header -->
<string name="onboarding_private_browsing_header">Navegue com privacidade</string>
<!-- text for the private browsing onboarding card description
The first parameter is an icon that represents private browsing -->
<string name="onboarding_private_browsing_description1">Para abrir uma aba privativa só uma vez: Toque no ícone %s.</string>
<string name="onboarding_private_browsing_description1">Para abrir manualmente uma aba privativa, toque no ícone %s.</string>
<!-- text for the private browsing onboarding card description, explaining how to always using private browsing -->
<string name="onboarding_private_browsing_always_description">Para abrir abas privativas todas as vezes: Mude a configuração da navegação privativa.</string>
<string name="onboarding_private_browsing_always_description">Para sempre abrir abas privativas, mude as configurações de navegação privativa.</string>
<!-- text for the private browsing onbording card button, that launches settings -->
<string name="onboarding_private_browsing_button">Abrir configurações</string>
<!-- text for the privacy notice onboarding card header -->
@ -1049,7 +1051,7 @@
<string name="onboarding_theme_picker_header">Escolha um tema</string>
<!-- text for the theme picker onboarding card description -->
<string name="onboarding_theme_picker_description1">Se quiser economizar bateria e poupar sua visão, use o modo escuro.</string>
<string name="onboarding_theme_picker_description1">Se quiser poupar sua vista e economizar bateria, use o modo escuro.</string>
<!-- Automatic theme setting (will follow device setting) -->
<string name="onboarding_theme_automatic_title">Automático</string>
<!-- Summary of automatic theme setting (will follow device setting) -->
@ -1106,13 +1108,13 @@
<!-- Preference for enhanced tracking protection for the standard protection settings -->
<string name="preference_enhanced_tracking_protection_standard_default_1">Padrão (predefinido)</string>
<!-- Preference description for enhanced tracking protection for the standard protection settings -->
<string name="preference_enhanced_tracking_protection_standard_description_3">Bloqueia menos rastreadores. Páginas são carregadas normalmente.</string>
<string name="preference_enhanced_tracking_protection_standard_description_3">Bloqueia menos rastreadores. As páginas são carregadas normalmente.</string>
<!-- Accessibility text for the Standard protection information icon -->
<string name="preference_enhanced_tracking_protection_standard_info_button">O que é bloqueado pela proteção padrão contra rastreamento</string>
<!-- Preference for enhanced tracking protection for the strict protection settings -->
<string name="preference_enhanced_tracking_protection_strict">Rigoroso</string>
<!-- Preference description for enhanced tracking protection for the strict protection settings -->
<string name="preference_enhanced_tracking_protection_strict_description_2">Bloqueia mais rastreadores, anúncios e notificações. Páginas são carregadas mais rápido, mas algumas coisas podem não funcionar.</string>
<string name="preference_enhanced_tracking_protection_strict_description_2">Bloqueia mais rastreadores, anúncios e notificações. As páginas são carregadas mais rápido, mas algumas coisas podem não funcionar.</string>
<!-- Accessibility text for the Strict protection information icon -->
<string name="preference_enhanced_tracking_protection_strict_info_button">O que é bloqueado pela proteção rigorosa contra rastreamento</string>
<!-- Preference for enhanced tracking protection for the custom protection settings -->
@ -1412,17 +1414,17 @@
<!-- Label that indicates a site is using a insecure connection -->
<string name="quick_settings_sheet_insecure_connection">Conexão não segura</string>
<!-- Confirmation message for a dialog confirming if the user wants to delete all the permissions for all sites-->
<string name="confirm_clear_permissions_on_all_sites">Tem certeza que deseja limpar todas as permissões de todos os sites?</string>
<string name="confirm_clear_permissions_on_all_sites">Tem certeza que quer limpar todas as permissões de todos os sites?</string>
<!-- Confirmation message for a dialog confirming if the user wants to delete all the permissions for a site-->
<string name="confirm_clear_permissions_site">Tem certeza que deseja limpar todas as permissões deste site?</string>
<string name="confirm_clear_permissions_site">Tem certeza que quer limpar todas as permissões deste site?</string>
<!-- Confirmation message for a dialog confirming if the user wants to set default value a permission for a site-->
<string name="confirm_clear_permission_site">Tem certeza que deseja limpar esta permissão deste site?</string>
<string name="confirm_clear_permission_site">Tem certeza que quer limpar esta permissão deste site?</string>
<!-- label shown when there are not site exceptions to show in the site exception settings -->
<string name="no_site_exceptions">Nenhuma exceção de sites</string>
<!-- Label for the Pocket default top site -->
<string name="pocket_top_articles">Artigos populares</string>
<!-- Bookmark deletion confirmation -->
<string name="bookmark_deletion_confirmation">Tem certeza que deseja excluir este favorito?</string>
<string name="bookmark_deletion_confirmation">Tem certeza que quer excluir este favorito?</string>
<!-- Browser menu button that adds a top site to the home fragment -->
<string name="browser_menu_add_to_top_sites">Adicionar preferido</string>
<!-- text shown before the issuer name to indicate who its verified by, parameter is the name of
@ -1433,7 +1435,7 @@
<!-- Login overflow menu edit button -->
<string name="login_menu_edit_button">Editar</string>
<!-- Message in delete confirmation dialog for logins -->
<string name="login_deletion_confirmation">Tem certeza de que deseja excluir esta conta?</string>
<string name="login_deletion_confirmation">Tem certeza que quer excluir esta conta?</string>
<!-- Positive action of a dialog asking to delete -->
<string name="dialog_delete_positive">Excluir</string>
<!-- The saved login options menu description. -->

@ -303,6 +303,8 @@
<string name="preferences_account_settings">Definições da conta</string>
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Abrir ligações em aplicações</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Gestor de transferências externo</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Extras</string>

@ -298,6 +298,8 @@
<string name="preferences_account_settings">Setările contului</string>
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Deschide linkuri în aplicații</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Manager extern pentru descărcări</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Suplimente</string>

@ -311,6 +311,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Открывать ссылки в приложениях</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Внешний менеджер загрузок</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Дополнения</string>

@ -308,6 +308,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Otvárať odkazy v aplikáciách</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Externý správca preberania súborov</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Doplnky</string>

@ -294,6 +294,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Odpiraj povezave v aplikacijah</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Zunanji upravitelj prenosov</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Dodatki</string>

@ -301,6 +301,8 @@
<string name="preferences_account_settings">Подешавања налога</string>
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Отвори везе у апликацијама</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Спољни менаџер преузимања</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Додаци</string>

@ -303,6 +303,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Buka tutumbu dina aplikasi</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Manajer undeuran éksternal</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Émbohan</string>

@ -306,6 +306,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Öppna länkar i appar</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Extern filhämtare</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Tillägg</string>

@ -303,6 +303,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">లంకెలను అనువర్తనాల్లో తెరువు</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">బయటి దింపుకోలు నిర్వాహకి</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">పొడగింతలు</string>

@ -304,6 +304,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Bağlantıları uygulamalarda aç</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Harici indirme yöneticisi</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Eklentiler</string>

@ -308,6 +308,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Відкривати посилання в програмах</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Зовнішній менеджер завантажень</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Додатки</string>

@ -301,6 +301,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">ایپس میں ربط کھولیں</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">بیرونی ڈاؤن لوڈ مینیجر</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">ایڈ اون</string>

@ -302,6 +302,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Mở liên kết trong ứng dụng</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Trình quản lý tải xuống bên ngoài</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Tiện ích</string>

@ -313,6 +313,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">在应用程序中打开链接</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">外部下载管理器</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">附加组件</string>

@ -309,6 +309,8 @@
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">用 App 開啟鏈結</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">外部下載管理員</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">附加元件</string>

@ -136,7 +136,8 @@
android:defaultValue="false"
android:icon="@drawable/ic_download"
android:key="@string/pref_key_external_download_manager"
android:title="@string/preferences_external_download_manager" />
android:title="@string/preferences_external_download_manager"
app:isPreferenceVisible="false"/>
<androidx.preference.SwitchPreference
android:defaultValue="true"

@ -54,6 +54,7 @@ class DefaultReaderModeControllerTest {
every { readerViewFeature.hideReaderView() } returns Unit
every { readerViewFeature.showReaderView() } returns Unit
every { readerViewFeature.showControls() } returns Unit
every { readerViewFeature.hideControls() } returns Unit
}
@Test
@ -61,6 +62,7 @@ class DefaultReaderModeControllerTest {
val controller = DefaultReaderModeController(featureWrapper, readerViewControlsBar)
controller.hideReaderView()
verify { readerViewFeature.hideReaderView() }
verify { readerViewFeature.hideControls() }
}
@Test

@ -2,7 +2,9 @@ package org.mozilla.fenix.components
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
@ -10,7 +12,9 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.Config
import org.mozilla.fenix.R
import org.mozilla.fenix.ReleaseChannel
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@ -87,6 +91,56 @@ class TrackingProtectionPolicyFactoryTest {
expected.assertPolicyEquals(always, checkPrivacy = false)
}
@Test
fun `cookiePurging must be available ONLY in nightly or debug`() {
mockkObject(Config)
for (channel in ReleaseChannel.values()) {
every { Config.channel } returns channel
val shouldCookiePurgingActive = channel.isNightlyOrDebug
val customSetting =
settingsForCustom(shouldBlockCookiesInCustom = true, blockCookiesSelection = "all")
val stringSetting = mockSettings(useStrict = true)
val recommendedSetting = mockSettings(useTrackingProtection = true)
for (setting in arrayOf(recommendedSetting, stringSetting, customSetting)) {
val factory = TrackingProtectionPolicyFactory(setting)
val privateOnly =
factory.createTrackingProtectionPolicy(normalMode = false, privateMode = true)
val normalOnly =
factory.createTrackingProtectionPolicy(normalMode = true, privateMode = false)
val always =
factory.createTrackingProtectionPolicy(normalMode = true, privateMode = true)
assertEquals(shouldCookiePurgingActive, privateOnly.cookiePurging)
assertEquals(shouldCookiePurgingActive, normalOnly.cookiePurging)
assertEquals(shouldCookiePurgingActive, always.cookiePurging)
}
}
}
@Test
fun `adaptPolicyToChannel MUST only update properties that have changed per given channel`() {
mockkObject(Config)
val policies = arrayOf(
TrackingProtectionPolicy.strict(), TrackingProtectionPolicy.recommended(),
TrackingProtectionPolicy.select()
)
for (channel in ReleaseChannel.values()) {
every { Config.channel } returns channel
val shouldCookiePurgingActive = channel.isNightlyOrDebug
for (policy in policies) {
val adaptedPolicy = policy.adaptPolicyToChannel()
policy.assertPolicyEquals(adaptedPolicy, checkPrivacy = false)
assertEquals(shouldCookiePurgingActive, adaptedPolicy.cookiePurging)
}
}
}
@Test
fun `GIVEN custom policy WHEN cookie policy social THEN tracking policy should have cookie policy allow non-trackers`() {
val expected = EngineSession.TrackingProtectionPolicy.select(

@ -148,4 +148,22 @@ class ViewTest {
every { view.getKeyboardHeight() } returns 100
assertEquals(true, view.isKeyboardVisible())
}
@Test
fun `getRectWithScreenLocation should transform getLocationInScreen method values`() {
val locationOnScreen = slot<IntArray>()
every { view.getLocationOnScreen(capture(locationOnScreen)) } answers {
locationOnScreen.captured[0] = 100
locationOnScreen.captured[1] = 200
}
every { view.width } returns 150
every { view.height } returns 250
val outRect = view.getRectWithScreenLocation()
assertEquals(100, outRect.left)
assertEquals(200, outRect.top)
assertEquals(250, outRect.right)
assertEquals(450, outRect.bottom)
}
}

@ -9,6 +9,8 @@ import io.mockk.spyk
import io.mockk.verifyOrder
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@ -19,6 +21,16 @@ internal class BookmarkAdapterTest {
private lateinit var bookmarkAdapter: BookmarkAdapter
private val item = BookmarkNode(
BookmarkNodeType.ITEM,
"456",
"123",
0,
"Mozilla",
"http://mozilla.org",
null
)
@Before
fun setup() {
bookmarkAdapter = spyk(
@ -30,7 +42,7 @@ internal class BookmarkAdapterTest {
fun `update adapter from tree of bookmark nodes, null tree returns empty list`() {
val tree = BookmarkNode(
BookmarkNodeType.FOLDER, "123", null, 0, "Mobile", null, listOf(
BookmarkNode(BookmarkNodeType.ITEM, "456", "123", 0, "Mozilla", "http://mozilla.org", null),
item,
BookmarkNode(BookmarkNodeType.SEPARATOR, "789", "123", 1, null, null, null),
BookmarkNode(
BookmarkNodeType.ITEM,
@ -52,4 +64,52 @@ internal class BookmarkAdapterTest {
bookmarkAdapter.notifyItemRangeRemoved(0, 3)
}
}
@Test
fun `items are the same if they have the same guids`() {
assertTrue(createSingleItemDiffUtil(item, item).areItemsTheSame(0, 0))
assertTrue(
createSingleItemDiffUtil(
item,
item.copy(title = "Wikipedia.org", url = "https://www.wikipedia.org")
).areItemsTheSame(0, 0)
)
assertFalse(
createSingleItemDiffUtil(
item,
item.copy(guid = "111")
).areItemsTheSame(0, 0)
)
}
@Test
fun `equal items have same contents unless their selected state changes`() {
assertTrue(createSingleItemDiffUtil(item, item).areContentsTheSame(0, 0))
assertFalse(
createSingleItemDiffUtil(item, item.copy(position = 1)).areContentsTheSame(0, 0)
)
assertFalse(
createSingleItemDiffUtil(
item,
item,
oldMode = BookmarkFragmentState.Mode.Selecting(setOf(item))
).areContentsTheSame(0, 0)
)
assertFalse(
createSingleItemDiffUtil(
item,
item,
newMode = BookmarkFragmentState.Mode.Selecting(setOf(item))
).areContentsTheSame(0, 0)
)
}
private fun createSingleItemDiffUtil(
oldItem: BookmarkNode,
newItem: BookmarkNode,
oldMode: BookmarkFragmentState.Mode = BookmarkFragmentState.Mode.Normal(),
newMode: BookmarkFragmentState.Mode = BookmarkFragmentState.Mode.Normal()
): BookmarkAdapter.BookmarkDiffUtil {
return BookmarkAdapter.BookmarkDiffUtil(listOf(oldItem), listOf(newItem), oldMode, newMode)
}
}

@ -0,0 +1,98 @@
/* 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.bookmarks
import android.content.Context
import androidx.appcompat.view.ContextThemeWrapper
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.concept.menu.candidate.TextStyle
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.library.bookmarks.BookmarkItemMenu.Item
@RunWith(FenixRobolectricTestRunner::class)
class BookmarkItemMenuTest {
private lateinit var context: Context
private lateinit var onItemTapped: (Item) -> Unit
private lateinit var menu: BookmarkItemMenu
@Before
fun setup() {
context = ContextThemeWrapper(testContext, R.style.NormalTheme)
onItemTapped = mockk(relaxed = true)
menu = BookmarkItemMenu(context, onItemTapped)
}
@Test
fun `delete item has special styling`() {
val deleteItem = menu.menuItems(BookmarkNodeType.SEPARATOR).last()
assertEquals("Delete", deleteItem.text)
assertEquals(
TextStyle(color = context.getColorFromAttr(R.attr.destructive)),
deleteItem.textStyle
)
deleteItem.onClick()
verify { onItemTapped(Item.Delete) }
}
@Test
fun `edit item appears for folders`() {
val folderItems = menu.menuItems(BookmarkNodeType.FOLDER)
assertEquals(2, folderItems.size)
val (edit, delete) = folderItems
assertEquals("Edit", edit.text)
edit.onClick()
verify { onItemTapped(Item.Edit) }
assertEquals("Delete", delete.text)
}
@Test
fun `all item appears for sites`() {
val siteItems = menu.menuItems(BookmarkNodeType.ITEM)
assertEquals(6, siteItems.size)
val (edit, copy, share, openInNewTab, openInPrivateTab, delete) = siteItems
assertEquals("Edit", edit.text)
assertEquals("Copy", copy.text)
assertEquals("Share", share.text)
assertEquals("Open in new tab", openInNewTab.text)
assertEquals("Open in private tab", openInPrivateTab.text)
assertEquals("Delete", delete.text)
edit.onClick()
verify { onItemTapped(Item.Edit) }
copy.onClick()
verify { onItemTapped(Item.Copy) }
share.onClick()
verify { onItemTapped(Item.Share) }
openInNewTab.onClick()
verify { onItemTapped(Item.OpenInNewTab) }
openInPrivateTab.onClick()
verify { onItemTapped(Item.OpenInPrivateTab) }
delete.onClick()
verify { onItemTapped(Item.Delete) }
}
private operator fun <T> List<T>.component6(): T {
return get(5)
}
}

@ -0,0 +1,87 @@
/* 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.bookmarks.viewholders
import androidx.appcompat.content.res.AppCompatResources
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.verify
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentInteractor
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
import org.mozilla.fenix.library.bookmarks.BookmarkPayload
class BookmarkFolderViewHolderTest {
@MockK
private lateinit var interactor: BookmarkFragmentInteractor
@MockK(relaxed = true)
private lateinit var siteItemView: LibrarySiteItemView
private lateinit var holder: BookmarkFolderViewHolder
private val folder = BookmarkNode(
type = BookmarkNodeType.FOLDER,
guid = "456",
parentGuid = "123",
position = 0,
title = "Folder",
url = null,
children = listOf()
)
@Before
fun setup() {
MockKAnnotations.init(this)
mockkStatic(AppCompatResources::class)
every { AppCompatResources.getDrawable(any(), any()) } returns mockk(relaxed = true)
holder = BookmarkFolderViewHolder(siteItemView, interactor)
}
@Test
fun `binds title and selected state`() {
holder.bind(folder, BookmarkFragmentState.Mode.Normal())
verify {
siteItemView.titleView.text = folder.title
siteItemView.overflowView.showAndEnable()
siteItemView.changeSelected(false)
}
holder.bind(folder, BookmarkFragmentState.Mode.Selecting(setOf(folder)))
verify {
siteItemView.titleView.text = folder.title
siteItemView.overflowView.hideAndDisable()
siteItemView.changeSelected(true)
}
}
@Test
fun `bind with payload of no changes does not rebind views`() {
holder.bind(
folder,
BookmarkFragmentState.Mode.Normal(),
BookmarkPayload(false, false, false, false)
)
verify(inverse = true) {
siteItemView.titleView.text = folder.title
siteItemView.overflowView.showAndEnable()
siteItemView.overflowView.hideAndDisable()
siteItemView.changeSelected(any())
}
}
}

@ -0,0 +1,144 @@
/* 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.bookmarks.viewholders
import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentInteractor
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentState
import org.mozilla.fenix.library.bookmarks.BookmarkPayload
class BookmarkItemViewHolderTest {
@MockK
private lateinit var interactor: BookmarkFragmentInteractor
@MockK(relaxed = true)
private lateinit var siteItemView: LibrarySiteItemView
private lateinit var holder: BookmarkItemViewHolder
private val item = BookmarkNode(
type = BookmarkNodeType.ITEM,
guid = "456",
parentGuid = "123",
position = 0,
title = "Mozilla",
url = "https://www.mozilla.org",
children = listOf()
)
@Before
fun setup() {
MockKAnnotations.init(this)
holder = BookmarkItemViewHolder(siteItemView, interactor)
}
@Test
fun `binds views for unselected item`() {
val mode = BookmarkFragmentState.Mode.Normal()
holder.bind(item, mode)
verify {
siteItemView.setSelectionInteractor(item, mode, interactor)
siteItemView.titleView.text = item.title
siteItemView.urlView.text = item.url
siteItemView.overflowView.showAndEnable()
siteItemView.changeSelected(false)
holder.setColorsAndIcons(item.url)
}
}
@Test
fun `binds views for selected item`() {
val mode = BookmarkFragmentState.Mode.Selecting(setOf(item))
holder.bind(item, mode)
verify {
siteItemView.setSelectionInteractor(item, mode, interactor)
siteItemView.titleView.text = item.title
siteItemView.urlView.text = item.url
siteItemView.overflowView.hideAndDisable()
siteItemView.changeSelected(true)
holder.setColorsAndIcons(item.url)
}
}
@Test
fun `bind with payload of no changes does not rebind views`() {
holder.bind(
item,
BookmarkFragmentState.Mode.Normal(),
BookmarkPayload(false, false, false, false)
)
verify(inverse = true) {
siteItemView.titleView.text = item.title
siteItemView.urlView.text = item.url
siteItemView.overflowView.showAndEnable()
siteItemView.overflowView.hideAndDisable()
siteItemView.changeSelected(any())
holder.setColorsAndIcons(item.url)
}
}
@Test
fun `binding an item with a null title uses the url as the title`() {
val item = item.copy(title = null)
holder.bind(item, BookmarkFragmentState.Mode.Normal())
verify { siteItemView.titleView.text = item.url }
}
@Test
fun `binding an item with a blank title uses the url as the title`() {
val item = item.copy(title = " ")
holder.bind(item, BookmarkFragmentState.Mode.Normal())
verify { siteItemView.titleView.text = item.url }
}
@Test
fun `rebinds title if item title is null and the item url has changed`() {
val item = item.copy(title = null)
holder.bind(
item,
BookmarkFragmentState.Mode.Normal(),
BookmarkPayload(
titleChanged = false,
urlChanged = true,
selectedChanged = false,
modeChanged = false
)
)
verify { siteItemView.titleView.text = item.url }
}
@Test
fun `rebinds title if item title is blank and the item url has changed`() {
val item = item.copy(title = " ")
holder.bind(
item,
BookmarkFragmentState.Mode.Normal(),
BookmarkPayload(
titleChanged = false,
urlChanged = true,
selectedChanged = false,
modeChanged = false
)
)
verify { siteItemView.titleView.text = item.url }
}
}

@ -0,0 +1,76 @@
/* 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.history
import android.content.Context
import androidx.appcompat.view.ContextThemeWrapper
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.concept.menu.candidate.TextStyle
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.library.history.HistoryItemMenu.Item
@RunWith(FenixRobolectricTestRunner::class)
class HistoryItemMenuTest {
private lateinit var context: Context
private lateinit var onItemTapped: (Item) -> Unit
private lateinit var menu: HistoryItemMenu
@Before
fun setup() {
context = ContextThemeWrapper(testContext, R.style.NormalTheme)
onItemTapped = mockk(relaxed = true)
menu = HistoryItemMenu(context, onItemTapped)
}
@Test
fun `delete item has special styling`() {
val deleteItem = menu.menuItems().last()
assertEquals("Delete", deleteItem.text)
assertEquals(
TextStyle(color = context.getColorFromAttr(R.attr.destructive)),
deleteItem.textStyle
)
deleteItem.onClick()
verify { onItemTapped(Item.Delete) }
}
@Test
fun `builds menu items`() {
val items = menu.menuItems()
assertEquals(5, items.size)
val (copy, share, openInNewTab, openInPrivateTab, delete) = items
assertEquals("Copy", copy.text)
assertEquals("Share", share.text)
assertEquals("Open in new tab", openInNewTab.text)
assertEquals("Open in private tab", openInPrivateTab.text)
assertEquals("Delete", delete.text)
copy.onClick()
verify { onItemTapped(Item.Copy) }
share.onClick()
verify { onItemTapped(Item.Share) }
openInNewTab.onClick()
verify { onItemTapped(Item.OpenInNewTab) }
openInPrivateTab.onClick()
verify { onItemTapped(Item.OpenInPrivateTab) }
delete.onClick()
verify { onItemTapped(Item.Delete) }
}
}

@ -4,20 +4,144 @@
package org.mozilla.fenix.search
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.search.provider.SearchEngineList
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.TabSessionState
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotSame
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
import org.mozilla.fenix.utils.Settings
@ExperimentalCoroutinesApi
class SearchFragmentStoreTest {
@MockK private lateinit var searchEngine: SearchEngine
@MockK private lateinit var searchProvider: FenixSearchEngineProvider
@MockK private lateinit var activity: HomeActivity
@MockK(relaxed = true) private lateinit var components: Components
@MockK(relaxed = true) private lateinit var settings: Settings
@Before
fun setup() {
MockKAnnotations.init(this)
every { activity.browsingModeManager } returns object : BrowsingModeManager {
override var mode: BrowsingMode = BrowsingMode.Normal
}
every { components.settings } returns settings
every { components.search.provider } returns searchProvider
every { searchProvider.getDefaultEngine(activity) } returns searchEngine
every { searchProvider.installedSearchEngines(activity) } returns SearchEngineList(
list = listOf(mockk(), mockk()),
default = searchEngine
)
}
@Test
fun `createInitialSearchFragmentState with no tab`() {
activity.browsingModeManager.mode = BrowsingMode.Normal
every { components.core.store.state } returns BrowserState()
every { settings.shouldShowSearchShortcuts } returns true
val expected = SearchFragmentState(
query = "",
url = "",
searchTerms = "",
searchEngineSource = SearchEngineSource.Default(searchEngine),
defaultEngineSource = SearchEngineSource.Default(searchEngine),
showSearchSuggestions = false,
showSearchSuggestionsHint = false,
showSearchShortcuts = true,
areShortcutsAvailable = true,
showClipboardSuggestions = false,
showHistorySuggestions = false,
showBookmarkSuggestions = false,
tabId = null,
pastedText = "pastedText",
searchAccessPoint = SearchAccessPoint.ACTION
)
assertEquals(
expected,
createInitialSearchFragmentState(
activity,
components,
tabId = null,
pastedText = "pastedText",
searchAccessPoint = SearchAccessPoint.ACTION
)
)
assertEquals(
expected.copy(tabId = "tabId"),
createInitialSearchFragmentState(
activity,
components,
tabId = "tabId",
pastedText = "pastedText",
searchAccessPoint = SearchAccessPoint.ACTION
)
)
}
@Test
fun `createInitialSearchFragmentState with tab`() {
activity.browsingModeManager.mode = BrowsingMode.Private
every { components.core.store.state } returns BrowserState(
tabs = listOf(
TabSessionState(
id = "tabId",
content = ContentState(
url = "https://example.com",
searchTerms = "search terms"
)
)
)
)
assertEquals(
SearchFragmentState(
query = "https://example.com",
url = "https://example.com",
searchTerms = "search terms",
searchEngineSource = SearchEngineSource.Default(searchEngine),
defaultEngineSource = SearchEngineSource.Default(searchEngine),
showSearchSuggestions = false,
showSearchSuggestionsHint = false,
showSearchShortcuts = false,
areShortcutsAvailable = true,
showClipboardSuggestions = false,
showHistorySuggestions = false,
showBookmarkSuggestions = false,
tabId = "tabId",
pastedText = "",
searchAccessPoint = SearchAccessPoint.SHORTCUT
),
createInitialSearchFragmentState(
activity,
components,
tabId = "tabId",
pastedText = "",
searchAccessPoint = SearchAccessPoint.SHORTCUT
)
)
}
@Test
fun updateQuery() = runBlocking {
val initialState = emptyDefaultState()
@ -33,7 +157,6 @@ class SearchFragmentStoreTest {
fun selectSearchShortcutEngine() = runBlocking {
val initialState = emptyDefaultState()
val store = SearchFragmentStore(initialState)
val searchEngine: SearchEngine = mockk()
store.dispatch(SearchFragmentAction.SearchShortcutEngineSelected(searchEngine)).join()
assertNotSame(initialState, store.state)
@ -91,11 +214,10 @@ class SearchFragmentStoreTest {
fun selectNewDefaultEngine() = runBlocking {
val initialState = emptyDefaultState()
val store = SearchFragmentStore(initialState)
val engine = mockk<SearchEngine>()
store.dispatch(SearchFragmentAction.SelectNewDefaultSearchEngine(engine)).join()
store.dispatch(SearchFragmentAction.SelectNewDefaultSearchEngine(searchEngine)).join()
assertNotSame(initialState, store.state)
assertEquals(SearchEngineSource.Default(engine), store.state.searchEngineSource)
assertEquals(SearchEngineSource.Default(searchEngine), store.state.searchEngineSource)
}
private fun emptyDefaultState(): SearchFragmentState = SearchFragmentState(
@ -112,6 +234,6 @@ class SearchFragmentStoreTest {
showClipboardSuggestions = false,
showHistorySuggestions = false,
showBookmarkSuggestions = false,
searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.NONE
searchAccessPoint = SearchAccessPoint.NONE
)
}

@ -0,0 +1,320 @@
/* 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.searchdialog
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.MockKAnnotations
import io.mockk.Runs
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.unmockkObject
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.search.SearchFragmentAction
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Settings
@ExperimentalCoroutinesApi
class SearchDialogControllerTest {
@MockK(relaxed = true) private lateinit var activity: HomeActivity
@MockK(relaxed = true) private lateinit var store: SearchDialogFragmentStore
@MockK(relaxed = true) private lateinit var navController: NavController
@MockK private lateinit var searchEngine: SearchEngine
@MockK(relaxed = true) private lateinit var metrics: MetricController
@MockK(relaxed = true) private lateinit var settings: Settings
@MockK private lateinit var sessionManager: SessionManager
@MockK(relaxed = true) private lateinit var clearToolbarFocus: () -> Unit
private lateinit var controller: SearchDialogController
@Before
fun setUp() {
MockKAnnotations.init(this)
mockkObject(MetricsUtils)
every { store.state.tabId } returns "test-tab-id"
every { store.state.searchEngineSource.searchEngine } returns searchEngine
every { sessionManager.select(any()) } just Runs
every { MetricsUtils.createSearchEvent(searchEngine, activity, any()) } returns null
controller = SearchDialogController(
activity = activity,
sessionManager = sessionManager,
store = store,
navController = navController,
settings = settings,
metrics = metrics,
clearToolbarFocus = clearToolbarFocus
)
}
@After
fun teardown() {
unmockkObject(MetricsUtils)
}
@Test
fun handleUrlCommitted() {
val url = "https://www.google.com/"
controller.handleUrlCommitted(url)
verify {
activity.openToBrowserAndLoad(
searchTermOrURL = url,
newTab = false,
from = BrowserDirection.FromSearchDialog,
engine = searchEngine
)
}
verify { metrics.track(Event.EnteredUrl(false)) }
}
@Test
fun handleSearchCommitted() {
val searchTerm = "Firefox"
controller.handleUrlCommitted(searchTerm)
verify {
activity.openToBrowserAndLoad(
searchTermOrURL = searchTerm,
newTab = false,
from = BrowserDirection.FromSearchDialog,
engine = searchEngine
)
}
verify { settings.incrementActiveSearchCount() }
}
@Test
fun handleCrashesUrlCommitted() {
val url = "about:crashes"
every { activity.packageName } returns "org.mozilla.fenix"
controller.handleUrlCommitted(url)
verify {
activity.startActivity(any())
}
}
@Test
fun handleMozillaUrlCommitted() {
val url = "moz://a"
controller.handleUrlCommitted(url)
verify {
activity.openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.MANIFESTO),
newTab = false,
from = BrowserDirection.FromSearchDialog,
engine = searchEngine
)
}
verify { metrics.track(Event.EnteredUrl(false)) }
}
@Test
fun handleEditingCancelled() = runBlockingTest {
controller.handleEditingCancelled()
verify {
clearToolbarFocus()
}
}
@Test
fun handleTextChangedNonEmpty() {
val text = "fenix"
controller.handleTextChanged(text)
verify { store.dispatch(SearchFragmentAction.UpdateQuery(text)) }
}
@Test
fun handleTextChangedEmpty() {
val text = ""
controller.handleTextChanged(text)
verify { store.dispatch(SearchFragmentAction.UpdateQuery(text)) }
}
@Test
fun `show search shortcuts when setting enabled AND query empty`() {
val text = ""
every { settings.shouldShowSearchShortcuts } returns true
controller.handleTextChanged(text)
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(true)) }
}
@Test
fun `show search shortcuts when setting enabled AND query equals url`() {
val text = "mozilla.org"
every { store.state.url } returns "mozilla.org"
every { settings.shouldShowSearchShortcuts } returns true
controller.handleTextChanged(text)
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(true)) }
}
@Test
fun `do not show search shortcuts when setting enabled AND query non-empty`() {
val text = "mozilla"
controller.handleTextChanged(text)
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) }
}
@Test
fun `do not show search shortcuts when setting disabled AND query empty AND url not matching query`() {
every { settings.shouldShowSearchShortcuts } returns false
val text = ""
controller.handleTextChanged(text)
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) }
}
@Test
fun `do not show search shortcuts when setting disabled AND query non-empty`() {
every { settings.shouldShowSearchShortcuts } returns false
val text = "mozilla"
controller.handleTextChanged(text)
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) }
}
@Test
fun handleUrlTapped() {
val url = "https://www.google.com/"
controller.handleUrlTapped(url)
verify {
activity.openToBrowserAndLoad(
searchTermOrURL = url,
newTab = false,
from = BrowserDirection.FromSearchDialog
)
}
verify { metrics.track(Event.EnteredUrl(false)) }
}
@Test
fun handleSearchTermsTapped() {
val searchTerms = "fenix"
controller.handleSearchTermsTapped(searchTerms)
verify {
activity.openToBrowserAndLoad(
searchTermOrURL = searchTerms,
newTab = false,
from = BrowserDirection.FromSearchDialog,
engine = searchEngine,
forceSearch = true
)
}
}
@Test
fun handleSearchShortcutEngineSelected() {
val searchEngine: SearchEngine = mockk(relaxed = true)
controller.handleSearchShortcutEngineSelected(searchEngine)
verify { store.dispatch(SearchFragmentAction.SearchShortcutEngineSelected(searchEngine)) }
verify { metrics.track(Event.SearchShortcutSelected(searchEngine, false)) }
}
@Test
fun handleClickSearchEngineSettings() {
val directions: NavDirections =
SearchDialogFragmentDirections.actionGlobalSearchEngineFragment()
controller.handleClickSearchEngineSettings()
verify { navController.navigateSafe(R.id.searchEngineFragment, directions) }
}
@Test
fun handleSearchShortcutsButtonClicked_alreadyOpen() {
every { store.state.showSearchShortcuts } returns true
controller.handleSearchShortcutsButtonClicked()
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) }
}
@Test
fun handleSearchShortcutsButtonClicked_notYetOpen() {
every { store.state.showSearchShortcuts } returns false
controller.handleSearchShortcutsButtonClicked()
verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(true)) }
}
@Test
fun handleExistingSessionSelected() {
val session = mockk<Session>()
controller.handleExistingSessionSelected(session)
verify { sessionManager.select(session) }
verify { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) }
}
@Test
fun handleExistingSessionSelected_tabId_nullSession() {
every { sessionManager.findSessionById("tab-id") } returns null
controller.handleExistingSessionSelected("tab-id")
verify(inverse = true) { sessionManager.select(any()) }
verify(inverse = true) { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) }
}
@Test
fun handleExistingSessionSelected_tabId() {
val session = mockk<Session>()
every { sessionManager.findSessionById("tab-id") } returns session
controller.handleExistingSessionSelected("tab-id")
verify { sessionManager.select(any()) }
verify { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) }
}
}

@ -1,87 +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.session
import android.content.Context
import io.mockk.Called
import io.mockk.MockKAnnotations
import io.mockk.confirmVerified
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.state.action.CustomTabListAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.createCustomTab
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@ExperimentalCoroutinesApi
@RunWith(FenixRobolectricTestRunner::class)
class NotificationSessionObserverTest {
private lateinit var observer: NotificationSessionObserver
private lateinit var store: BrowserStore
@MockK private lateinit var context: Context
@MockK(relaxed = true) private lateinit var notificationService: SessionNotificationService.Companion
@Before
fun before() {
MockKAnnotations.init(this)
store = BrowserStore()
every { context.components.core.store } returns store
observer = NotificationSessionObserver(context, notificationService)
NotificationSessionObserver.isStartedFromPrivateShortcut = false
}
@Test
fun `GIVEN session is private and non-custom WHEN it is added THEN notification service should be started`() = runBlocking {
val privateSession = createTab("https://firefox.com", private = true)
store.dispatch(TabListAction.AddTabAction(privateSession)).join()
observer.start()
verify(exactly = 1) { notificationService.start(context, false) }
confirmVerified(notificationService)
}
@Test
fun `GIVEN session is not private WHEN it is added THEN notification service should not be started`() = runBlocking {
val normalSession = createTab("https://firefox.com")
val customSession = createCustomTab("https://firefox.com")
observer.start()
verify { notificationService wasNot Called }
store.dispatch(TabListAction.AddTabAction(normalSession)).join()
verify(exactly = 0) { notificationService.start(context, false) }
store.dispatch(CustomTabListAction.AddCustomTabAction(customSession)).join()
verify(exactly = 0) { notificationService.start(context, false) }
}
@Test
fun `GIVEN session is custom tab WHEN it is added THEN notification service should not be started`() = runBlocking {
val privateCustomSession = createCustomTab("https://firefox.com").let {
it.copy(content = it.content.copy(private = true))
}
val customSession = createCustomTab("https://firefox.com")
observer.start()
verify { notificationService wasNot Called }
store.dispatch(CustomTabListAction.AddCustomTabAction(privateCustomSession)).join()
verify(exactly = 0) { notificationService.start(context, false) }
store.dispatch(CustomTabListAction.AddCustomTabAction(customSession)).join()
verify(exactly = 0) { notificationService.start(context, false) }
}
}

@ -0,0 +1,64 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.session
import android.content.ComponentName
import android.content.Intent
import io.mockk.every
import io.mockk.mockk
import mozilla.components.feature.privatemode.notification.AbstractPrivateNotificationService.Companion.ACTION_ERASE
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.HomeActivity.Companion.PRIVATE_BROWSING_MODE
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.robolectric.Robolectric
import org.robolectric.Shadows.shadowOf
import org.robolectric.android.controller.ServiceController
@RunWith(FenixRobolectricTestRunner::class)
class PrivateNotificationServiceTest {
private lateinit var controller: ServiceController<PrivateNotificationService>
@Before
fun setup() {
val store = testContext.components.core.store
every { store.dispatch(any()) } returns mockk()
controller = Robolectric.buildService(
PrivateNotificationService::class.java,
Intent(ACTION_ERASE)
)
}
@Test
fun `service opens home activity with PBM flag set to true`() {
PrivateNotificationService.isStartedFromPrivateShortcut = true
val service = shadowOf(controller.get())
controller.startCommand(0, 0)
val intent = service.nextStartedActivity
assertEquals(ComponentName(testContext, HomeActivity::class.java), intent.component)
assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK, intent.flags)
assertEquals(true, intent.extras?.getBoolean(PRIVATE_BROWSING_MODE))
}
@Test
fun `service opens home activity with PBM flag set to false`() {
PrivateNotificationService.isStartedFromPrivateShortcut = false
val service = shadowOf(controller.get())
controller.startCommand(0, 0)
val intent = service.nextStartedActivity
assertEquals(ComponentName(testContext, HomeActivity::class.java), intent.component)
assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK, intent.flags)
assertEquals(false, intent.extras?.getBoolean(PRIVATE_BROWSING_MODE))
}
}

@ -1,27 +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.session
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class SessionNotificationServiceTest {
@Test
fun `Service keeps tracked of started state`() {
assertFalse(SessionNotificationService.started)
SessionNotificationService.start(testContext, false)
assertTrue(SessionNotificationService.started)
SessionNotificationService.stop(testContext)
assertFalse(SessionNotificationService.started)
}
}

@ -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.settings.about
import android.widget.ListView
import android.widget.TextView
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.robolectric.Robolectric
import org.robolectric.Shadows.shadowOf
import org.robolectric.shadows.ShadowAlertDialog
@RunWith(FenixRobolectricTestRunner::class)
class AboutLibrariesActivityTest {
@Test
fun `activity should display licenses`() {
val activity = Robolectric.buildActivity(AboutLibrariesActivity::class.java).create().get()
val listView = activity.findViewById<ListView>(R.id.about_libraries_listview)
assertTrue(0 < listView.count)
}
@Test
fun `item click should open license dialog`() {
val activity = Robolectric.buildActivity(AboutLibrariesActivity::class.java).create().get()
val listView = activity.findViewById<ListView>(R.id.about_libraries_listview)
val listViewShadow = shadowOf(listView)
listViewShadow.clickFirstItemContainingText("org.mozilla.geckoview:geckoview")
val alertDialogShadow = ShadowAlertDialog.getLatestDialog()
assertTrue(alertDialogShadow.isShowing)
val alertDialogText = alertDialogShadow
.findViewById<TextView>(android.R.id.message)
.text
.toString()
assertTrue(alertDialogText.contains("MPL"))
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save