Merge branch 'mozilla_main' into fork

pull/159/head
Abhijit Valluri 4 years ago
commit 8d84ec8ef5

18
.github/CODEOWNERS vendored

@ -14,14 +14,20 @@
# to their GitHub account, for example user@example.com.
# https://help.github.com/articles/about-codeowners/
# WARNING: if there is a single syntax error in this file, CODEOWNERS
# WILL NOT WORK AT ALL. Please be careful when editing this file.
#
# You can use the technique described in this blog post to validate
# the paths you specify in .gitignore:
# http://www.benjaminoakes.com/git/2018/08/10/Testing-changes-to-GitHub-CODEOWNERS/
# By default the Android Components team will be the owner for everything in
# the repo. Unless a later match takes precedence.
* @mozilla-mobile/ACT @mozilla-mobile/fenix
/.cron.yml /@mozilla-mobile/releng @mozilla-mobile/fenix
/.taskcluster.yml /@mozilla-mobile/releng @mozilla-mobile/fenix
/.cron.yml @mozilla-mobile/releng @mozilla-mobile/fenix
/.taskcluster.yml @mozilla-mobile/releng @mozilla-mobile/fenix
/automation/ @mozilla-mobile/releng @mozilla-mobile/fenix
/taskcluster/ /@mozilla-mobile/releng @mozilla-mobile/fenix
/taskcluster/ @mozilla-mobile/releng @mozilla-mobile/fenix
/.github/ @mozilla-mobile/releng @mozilla-mobile/fenix
# --- PERFORMANCE START --- #
@ -30,6 +36,12 @@
# these changes (for now) but to be aware of them. Please let us know
# if the CODEOWNERS system makes this impractical. We're available at
# #perf-android-frontend on Matrix.
# The perf team is relying on CODEOWNERS to catch regressions. If
# there is a single syntax error in the file, no rules will work.
# Therefore, we make the Perfomance team code owners of this file.
/.github/CODEOWNERS @mozilla-mobile/Performance
/app/src/*/java/org/mozilla/fenix/perf/** @mozilla-mobile/Performance
*.pro @mozilla-mobile/Performance
*proguard* @mozilla-mobile/Performance

@ -32,7 +32,7 @@ android {
resValue "bool", "IS_DEBUG", "false"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "false"
buildConfigField "String", "AMO_ACCOUNT", "\"mozilla\""
buildConfigField "String", "AMO_COLLECTION", "\"3204bb44a6ef44d39ee34917f28055\""
buildConfigField "String", "AMO_COLLECTION", "\"83a9cccfe6e24a34bd7b155ff9ee32\""
def deepLinkSchemeValue = "fenix-dev"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
@ -60,14 +60,14 @@ android {
shrinkResources false
minifyEnabled false
applicationIdSuffix ".fenix.debug"
buildConfigField "String", "AMO_COLLECTION", "\"83a9cccfe6e24a34bd7b155ff9ee32\""
buildConfigField "String", "AMO_COLLECTION", "\"7dfae8669acc4312a65e8ba5553036\""
resValue "bool", "IS_DEBUG", "true"
pseudoLocalesEnabled true
}
nightly releaseTemplate >> {
applicationIdSuffix ".fenix"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
buildConfigField "String", "AMO_COLLECTION", "\"83a9cccfe6e24a34bd7b155ff9ee32\""
buildConfigField "String", "AMO_COLLECTION", "\"7dfae8669acc4312a65e8ba5553036\""
def deepLinkSchemeValue = "fenix-nightly"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = ["deepLinkScheme": deepLinkSchemeValue]

@ -72,9 +72,9 @@ class HomeScreenTest {
verifyGetToKnowHeader()
// See What's new
scrollToElementByText("See whats new")
verifyWhatsNewHeader()
verifyWhatsNewLink()
// scrollToElementByText("See whats new")
// verifyWhatsNewHeader()
// verifyWhatsNewLink()
// Automatic privacy
scrollToElementByText("Automatic privacy")

@ -16,7 +16,6 @@ import androidx.annotation.IdRes
enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromGlobal(0),
FromHome(R.id.homeFragment),
FromSearch(R.id.searchFragment),
FromSearchDialog(R.id.searchDialogFragment),
FromSettings(R.id.settingsFragment),
FromSyncedTabs(R.id.syncedTabsFragment),

@ -21,11 +21,6 @@ object FeatureFlags {
*/
val syncedTabsInTabsTray = Config.channel.isNightlyOrDebug
/**
* Enables the new search experience
*/
const val newSearchExperience = true
/**
* Enables showing the top frequently visited sites
*/

@ -32,6 +32,7 @@ import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@ -86,8 +87,7 @@ import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.search.SearchFragmentDirections
import org.mozilla.fenix.searchdialog.SearchDialogFragmentDirections
import org.mozilla.fenix.search.SearchDialogFragmentDirections
import org.mozilla.fenix.session.PrivateNotificationService
import org.mozilla.fenix.settings.SettingsFragmentDirections
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
@ -261,10 +261,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
lifecycleScope.launch {
// Make sure accountManager is initialized.
components.backgroundServices.accountManager.initAsync().await()
components.backgroundServices.accountManager.start()
// If we're authenticated, kick-off a sync and a device state refresh.
components.backgroundServices.accountManager.authenticatedAccount()?.let {
components.backgroundServices.accountManager.syncNowAsync(
components.backgroundServices.accountManager.syncNow(
SyncReason.Startup,
debounce = true
)
@ -284,10 +284,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
settings().wasDefaultBrowserOnLastResume = settings().isDefaultBrowser()
if (!settings().manuallyCloseTabs) {
components.core.store.state.tabs.filter {
val toClose = components.core.store.state.tabs.filter {
(System.currentTimeMillis() - it.lastAccess) > settings().getTabTimeout()
}.forEach {
components.useCases.tabsUseCases.removeTab(it.id)
}
// Removal needs to happen on the main thread.
lifecycleScope.launch(Main) {
toClose.forEach { components.useCases.tabsUseCases.removeTab(it.id) }
}
}
}
@ -670,8 +672,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
NavGraphDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHome ->
HomeFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSearch ->
SearchFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSearchDialog ->
SearchDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSettings ->

@ -7,6 +7,8 @@ package org.mozilla.fenix.addons
import androidx.navigation.NavController
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.navigateSafe
/**
* View used for managing add-ons.
@ -37,7 +39,7 @@ class AddonsManagementView(
AddonsManagementFragmentDirections.actionAddonsManagementFragmentToInstalledAddonDetails(
addon
)
navController.navigate(directions)
navController.navigateSafe(R.id.addonsManagementFragment, directions)
}
private fun showDetailsFragment(addon: Addon) {
@ -45,7 +47,7 @@ class AddonsManagementView(
AddonsManagementFragmentDirections.actionAddonsManagementFragmentToAddonDetailsFragment(
addon
)
navController.navigate(directions)
navController.navigateSafe(R.id.addonsManagementFragment, directions)
}
private fun showNotYetSupportedAddonFragment(unsupportedAddons: List<Addon>) {

@ -19,6 +19,7 @@ import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
@ -110,6 +111,7 @@ import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.home.SharedViewModel
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.allowUndo
@ -206,6 +208,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
}
private val homeViewModel: HomeScreenViewModel by activityViewModels {
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
}
@Suppress("ComplexMethod", "LongMethod")
@CallSuper
protected open fun initializeUI(view: View): Session? {
@ -245,7 +251,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
readerModeController = readerMenuController,
sessionManager = requireComponents.core.sessionManager,
engineView = engineView,
browserAnimator = browserAnimator,
homeViewModel = homeViewModel,
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
onTabCounterClicked = {
thumbnailsFeature.get()?.requestScreenshot()
@ -812,7 +818,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
@CallSuper
override fun onSessionSelected(session: Session) {
updateThemeForSession(session)
if (!this.isRemoving) {
updateThemeForSession(session)
}
if (!browserInitialized) {
// Initializing a new coroutineScope to avoid ConcurrentModificationException in ObserverRegistry
// This will be removed when ObserverRegistry is deprecated by browser-state.
@ -849,7 +857,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
@CallSuper
override fun onPause() {
super.onPause()
if (findNavController().currentDestination?.id != R.id.searchFragment) {
if (findNavController().currentDestination?.id != R.id.searchDialogFragment) {
view?.hideKeyboard()
}
}
@ -941,7 +949,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
true
} else {
if (session.hasParentSession) {
requireComponents.useCases.tabsUseCases.removeTab(session)
// The removeTab use case does not currently select a parent session, so
// we are using sessionManager.remove
requireComponents.core.sessionManager.remove(
session,
selectParentIfExists = true
)
}
// We want to return to home if this session didn't have a parent session to select.
val goToOverview = !session.hasParentSession
@ -1104,6 +1117,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
.show()
activity?.enterToImmersiveMode()
browserToolbarView.view.isVisible = false
val browserEngine = swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams
browserEngine.bottomMargin = 0
engineView.setDynamicToolbarMaxHeight(0)
browserToolbarView.expand()

@ -160,7 +160,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
}
session?.register(toolbarSessionObserver, viewLifecycleOwner, autoPause = true)
if (settings.shouldShowOpenInAppBanner && session != null) {
if (settings.shouldShowOpenInAppCfr && session != null) {
openInAppOnboardingObserver = OpenInAppOnboardingObserver(
context = context,
navController = findNavController(),

@ -13,6 +13,7 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import kotlinx.android.synthetic.main.info_banner.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
/**
* Displays an Info Banner in the specified container with a message and an optional action.
@ -25,12 +26,15 @@ import org.mozilla.fenix.R
* @param actionText - The text on the action to perform button
* @param actionToPerform - The action to be performed on action button press
*/
@SuppressWarnings("LongParameterList")
class InfoBanner(
private val context: Context,
private val container: ViewGroup,
private val message: String,
private val dismissText: String,
private val actionText: String? = null,
private val dismissByHiding: Boolean = false,
private val dismissAction: (() -> Unit)? = null,
private val actionToPerform: (() -> Unit)? = null
) {
@SuppressLint("InflateParams")
@ -54,12 +58,15 @@ class InfoBanner(
params.width = MATCH_PARENT
bannerLayout.dismiss.setOnClickListener {
dismiss()
dismissAction?.invoke()
if (dismissByHiding) { bannerLayout.visibility = GONE } else { dismiss() }
}
bannerLayout.action.setOnClickListener {
actionToPerform?.invoke()
}
context.settings().lastCfrShownTimeInMillis = System.currentTimeMillis()
}
internal fun dismiss() {

@ -42,7 +42,7 @@ class OpenInAppOnboardingObserver(
if (!loading &&
!settings.openLinksInExternalApp &&
settings.shouldShowOpenInAppBanner &&
settings.shouldShowOpenInAppCfr &&
appLink(session.url).hasExternalApp()
) {
infoBanner = InfoBanner(

@ -35,7 +35,7 @@ class SearchWidgetCFR(
fun displayIfNecessary() {
if (settings.isInSearchWidgetExperiment &&
settings.shouldDisplaySearchWidgetCFR() &&
settings.shouldDisplaySearchWidgetCfr() &&
!isShown
) {
isShown = true
@ -45,6 +45,7 @@ class SearchWidgetCFR(
@Suppress("InflateParams")
private fun showSearchWidgetCFR() {
settings.lastCfrShownTimeInMillis = System.currentTimeMillis()
settings.incrementSearchWidgetCFRDisplayed()
val searchWidgetCFRDialog = Dialog(context)

@ -5,13 +5,11 @@
package org.mozilla.fenix.components
import android.content.Context
import android.content.SharedPreferences
import android.os.StrictMode
import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
@ -77,9 +75,18 @@ class AccountAbnormalities(
private val logger = Logger("AccountAbnormalities")
private val prefs = StrictMode.allowThreadDiskReads().resetPoliciesAfter {
context.getSharedPreferences(PREF_FXA_ABNORMALITIES, Context.MODE_PRIVATE)
}
private val prefs: SharedPreferences
private val hadAccountPrior: Boolean
init {
val prefPair = StrictMode.allowThreadDiskReads().resetPoliciesAfter {
val p = context.getSharedPreferences(PREF_FXA_ABNORMALITIES, Context.MODE_PRIVATE)
val a = p.getBoolean(KEY_HAS_ACCOUNT, false)
Pair(p, a)
}
prefs = prefPair.first
hadAccountPrior = prefPair.second
}
/**
* Once [accountManager] is initialized, queries it to detect abnormal account states.
@ -89,37 +96,28 @@ class AccountAbnormalities(
* @param initResult A deferred result of initializing [accountManager].
* @return A [Unit] deferred, resolved once [initResult] is resolved and state is processed for abnormalities.
*/
fun accountManagerInitializedAsync(
accountManager: FxaAccountManager,
initResult: Deferred<Unit>
): Deferred<Unit> {
fun accountManagerStarted(
accountManager: FxaAccountManager
) {
check(!accountManagerConfigured) { "accountManagerStarted called twice" }
accountManagerConfigured = true
return CoroutineScope(coroutineContext).async {
// Wait for the account manager to finish initializing. If it's queried before the
// "init" deferred returns, we'll get inaccurate results.
initResult.await()
// Account manager finished initialization, we can now query it for the account state
// and see if it doesn't match our expectations.
// Behaviour considered abnormal:
// - we had an account before, and it's no longer present during startup
// We use a flag in prefs to keep track of the fact that we have an authenticated
// account. This works because our account state is persisted in the application's
// directory, same as SharedPreferences. If user clears application data, both the
// fxa state and our flag will be removed.
val hadAccountBefore = prefs.getBoolean(KEY_HAS_ACCOUNT, false)
val hasAccountNow = accountManager.authenticatedAccount() != null
if (hadAccountBefore && !hasAccountNow) {
prefs.edit().putBoolean(KEY_HAS_ACCOUNT, false).apply()
logger.warn("Missing expected account on startup")
crashReporter.submitCaughtException(
AbnormalFxaEvent.MissingExpectedAccountAfterStartup()
)
}
// Behaviour considered abnormal:
// - we had an account before, and it's no longer present during startup
// We use a flag in prefs to keep track of the fact that we have an authenticated
// account. This works because our account state is persisted in the application's
// directory, same as SharedPreferences. If user clears application data, both the
// fxa state and our flag will be removed.
val hasAccountNow = accountManager.authenticatedAccount() != null
if (hadAccountPrior && !hasAccountNow) {
prefs.edit().putBoolean(KEY_HAS_ACCOUNT, false).apply()
logger.warn("Missing expected account on startup")
crashReporter.submitCaughtException(
AbnormalFxaEvent.MissingExpectedAccountAfterStartup()
)
}
}
@ -152,8 +150,7 @@ class AccountAbnormalities(
}
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
check(accountManagerConfigured) { "onAuthenticated before account manager was configured" }
// Not checking state of accountManagerConfigured because we'll race against account manager's start.
onAuthenticatedCalled = true
// We don't check if KEY_HAS_ACCOUNT was already true: we will see onAuthenticated on every

@ -8,19 +8,22 @@ import android.content.Context
import android.os.Build
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
import mozilla.components.browser.storage.sync.RemoteTabsStorage
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.DeviceCapability
import mozilla.components.concept.sync.DeviceConfig
import mozilla.components.concept.sync.DeviceType
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.feature.accounts.push.FxaPushSupportFeature
import mozilla.components.feature.accounts.push.SendTabFeature
import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
import mozilla.components.lib.crash.CrashReporter
import mozilla.components.service.fxa.DeviceConfig
import mozilla.components.service.fxa.PeriodicSyncConfig
import mozilla.components.service.fxa.ServerConfig
import mozilla.components.service.fxa.SyncConfig
import mozilla.components.service.fxa.SyncEngine
@ -86,7 +89,7 @@ class BackgroundServices(
@VisibleForTesting
val supportedEngines =
setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords, SyncEngine.Tabs)
private val syncConfig = SyncConfig(supportedEngines, syncPeriodInMinutes = 240L) // four hours
private val syncConfig = SyncConfig(supportedEngines, PeriodicSyncConfig(periodMinutes = 240)) // four hours
init {
/* Make the "history", "bookmark", "passwords", and "tabs" stores accessible to workers
@ -156,10 +159,10 @@ class BackgroundServices(
SyncedTabsIntegration(context, accountManager).launch()
accountAbnormalities.accountManagerInitializedAsync(
accountManager,
accountManager.initAsync()
)
MainScope().launch {
accountManager.start()
accountAbnormalities.accountManagerStarted(accountManager)
}
}.also {
accountManagerAvailableQueue.ready()
}
@ -180,31 +183,33 @@ internal class TelemetryAccountObserver(
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
when (authType) {
// User signed-in into an existing FxA account.
AuthType.Signin ->
metricController.track(Event.SyncAuthSignIn)
AuthType.Signin -> Event.SyncAuthSignIn
// User created a new FxA account.
AuthType.Signup ->
metricController.track(Event.SyncAuthSignUp)
AuthType.Signup -> Event.SyncAuthSignUp
// User paired to an existing account via QR code scanning.
AuthType.Pairing ->
metricController.track(Event.SyncAuthPaired)
AuthType.Pairing -> Event.SyncAuthPaired
// User signed-in into an FxA account shared from another locally installed app using the reuse flow.
AuthType.MigratedReuse -> Event.SyncAuthFromSharedReuse
// User signed-in into an FxA account shared from another locally installed app
// (e.g. Fennec).
AuthType.Shared ->
metricController.track(Event.SyncAuthFromShared)
// User signed-in into an FxA account shared from another locally installed app using the copy flow.
AuthType.MigratedCopy -> Event.SyncAuthFromSharedCopy
// Account Manager recovered a broken FxA auth state, without direct user involvement.
AuthType.Recovered ->
metricController.track(Event.SyncAuthRecovered)
AuthType.Recovered -> Event.SyncAuthRecovered
// User signed-in into an FxA account via unknown means.
// Exact mechanism identified by the 'action' param.
is AuthType.OtherExternal ->
metricController.track(Event.SyncAuthOtherExternal)
is AuthType.OtherExternal -> Event.SyncAuthOtherExternal
// Account restored from a hydrated state on disk (e.g. during startup).
AuthType.Existing -> null
}?.let {
metricController.track(it)
}
// Used by Leanplum as a context variable.
settings.fxaSignedIn = true
}

@ -75,8 +75,8 @@ class TabCollectionStorage(
return collectionStorage.getTabCollectionsCount()
}
fun getCollections(limit: Int = 20): LiveData<List<TabCollection>> {
return collectionStorage.getCollections(limit).asLiveData()
fun getCollections(): LiveData<List<TabCollection>> {
return collectionStorage.getCollections().asLiveData()
}
fun getCollectionsPaged(): DataSource.Factory<Int, TabCollection> {

@ -67,7 +67,8 @@ sealed class Event {
object SyncAuthPaired : Event()
object SyncAuthRecovered : Event()
object SyncAuthOtherExternal : Event()
object SyncAuthFromShared : Event()
object SyncAuthFromSharedReuse : Event()
object SyncAuthFromSharedCopy : Event()
object SyncAccountOpened : Event()
object SyncAccountClosed : Event()
object SyncAccountSyncNow : Event()

@ -269,7 +269,7 @@ private val Event.wrapper: EventWrapper<*>?
is Event.SyncAuthOtherExternal -> EventWrapper<NoExtraKeys>(
{ SyncAuth.otherExternal.record(it) }
)
is Event.SyncAuthFromShared -> EventWrapper<NoExtraKeys>(
is Event.SyncAuthFromSharedReuse, Event.SyncAuthFromSharedCopy -> EventWrapper<NoExtraKeys>(
{ SyncAuth.autoLogin.record(it) }
)
is Event.SyncAuthRecovered -> EventWrapper<NoExtraKeys>(

@ -42,7 +42,7 @@ private val Event.name: String?
is Event.CollectionTabRestored -> "E_Collection_Tab_Opened"
is Event.SyncAuthSignUp -> "E_FxA_New_Signup"
is Event.SyncAuthSignIn, Event.SyncAuthPaired, Event.SyncAuthOtherExternal -> "E_Sign_In_FxA"
is Event.SyncAuthFromShared -> "E_Sign_In_FxA_Fennec_to_Fenix"
is Event.SyncAuthFromSharedCopy, Event.SyncAuthFromSharedReuse -> "E_Sign_In_FxA_Fennec_to_Fenix"
is Event.SyncAuthSignOut -> "E_Sign_Out_FxA"
is Event.ClearedPrivateData -> "E_Cleared_Private_Data"
is Event.DismissedOnboarding -> "E_Dismissed_Onboarding"

@ -9,10 +9,8 @@ import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.engine.EngineView
import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator
import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
@ -23,6 +21,7 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeScreenViewModel
/**
* An interface that handles the view manipulation of the BrowserToolbar, triggered by the Interactor
@ -44,9 +43,8 @@ class DefaultBrowserToolbarController(
private val readerModeController: ReaderModeController,
private val sessionManager: SessionManager,
private val engineView: EngineView,
private val browserAnimator: BrowserAnimator,
private val homeViewModel: HomeScreenViewModel,
private val customTabSession: Session?,
private val useNewSearchExperience: Boolean = FeatureFlags.newSearchExperience,
private val onTabCounterClicked: () -> Unit,
private val onCloseTab: (Session) -> Unit
) : BrowserToolbarController {
@ -55,27 +53,14 @@ class DefaultBrowserToolbarController(
get() = customTabSession ?: sessionManager.selectedSession
override fun handleToolbarPaste(text: String) {
if (useNewSearchExperience) {
navController.nav(
R.id.browserFragment,
BrowserFragmentDirections.actionGlobalSearchDialog(
sessionId = currentSession?.id,
pastedText = text
),
getToolbarNavOptions(activity)
)
} else {
browserAnimator.captureEngineViewAndDrawStatically {
navController.nav(
R.id.browserFragment,
BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
sessionId = currentSession?.id,
pastedText = text
),
getToolbarNavOptions(activity)
)
}
}
navController.nav(
R.id.browserFragment,
BrowserFragmentDirections.actionGlobalSearchDialog(
sessionId = currentSession?.id,
pastedText = text
),
getToolbarNavOptions(activity)
)
}
override fun handleToolbarPasteAndGo(text: String) {
@ -94,26 +79,13 @@ class DefaultBrowserToolbarController(
override fun handleToolbarClick() {
metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.BROWSER))
if (useNewSearchExperience) {
navController.nav(
R.id.browserFragment,
BrowserFragmentDirections.actionGlobalSearchDialog(
currentSession?.id
),
getToolbarNavOptions(activity)
)
} else {
browserAnimator.captureEngineViewAndDrawStatically {
navController.nav(
R.id.browserFragment,
BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
currentSession?.id
),
getToolbarNavOptions(activity)
)
}
}
navController.nav(
R.id.browserFragment,
BrowserFragmentDirections.actionGlobalSearchDialog(
currentSession?.id
),
getToolbarNavOptions(activity)
)
}
override fun handleTabCounterClick() {
@ -138,14 +110,15 @@ class DefaultBrowserToolbarController(
if (sessionManager.sessionsOfType(it.private).count() == 1) {
// The tab tray always returns to normal mode so do that here too
activity.browsingModeManager.mode = BrowsingMode.Normal
homeViewModel.sessionToDelete = it.id
navController.navigate(
BrowserFragmentDirections.actionGlobalHome(
sessionToDelete = it.id
)
BrowserFragmentDirections.actionGlobalHome()
)
} else {
onCloseTab.invoke(it)
activity.components.useCases.tabsUseCases.removeTab.invoke(it)
// The removeTab use case does not currently select a parent session, so
// we are using sessionManager.remove
sessionManager.remove(it, selectParentIfExists = true)
}
}
}

@ -14,6 +14,7 @@ fun DownloadItem.getIcon(): Int {
return when {
fileName?.endsWith("apk") == true -> R.drawable.ic_file_type_apk
fileName?.endsWith("zip") == true -> R.drawable.ic_file_type_zip
fileName?.endsWith("pdf") == true -> R.drawable.ic_file_type_document
else -> R.drawable.ic_file_type_default
}
}

@ -33,7 +33,6 @@ import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -72,7 +71,6 @@ import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.content.res.resolveAttribute
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
@ -119,8 +117,8 @@ class HomeFragment : Fragment() {
private val args by navArgs<HomeFragmentArgs>()
private lateinit var bundleArgs: Bundle
private val homeViewModel: HomeScreenViewModel by viewModels {
ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
private val homeViewModel: HomeScreenViewModel by activityViewModels {
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
}
private val snackbarAnchorView: View?
@ -320,7 +318,8 @@ class HomeFragment : Fragment() {
)
view.homeAppBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = resources.getDimensionPixelSize(R.dimen.home_fragment_top_toolbar_header_margin)
topMargin =
resources.getDimensionPixelSize(R.dimen.home_fragment_top_toolbar_header_margin)
}
}
ToolbarPosition.BOTTOM -> {
@ -430,8 +429,7 @@ class HomeFragment : Fragment() {
// We call this onLayout so that the bottom bar width is correctly set for us to center
// the CFR in.
view.toolbar_wrapper.doOnLayout {
val willNavigateToSearch =
!bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience
val willNavigateToSearch = !bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR)
if (!browsingModeManager.mode.isPrivate && !willNavigateToSearch) {
SearchWidgetCFR(
context = view.context,
@ -453,7 +451,7 @@ class HomeFragment : Fragment() {
updateTabCounter(it)
}
bundleArgs.getString(SESSION_TO_DELETE)?.also {
homeViewModel.sessionToDelete?.also {
if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) {
removeAllTabsAndShowSnackbar(it)
} else {
@ -461,9 +459,11 @@ class HomeFragment : Fragment() {
}
}
homeViewModel.sessionToDelete = null
updateTabCounter(requireComponents.core.store.state)
if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience) {
if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR)) {
navigateToSearch()
}
}
@ -471,7 +471,8 @@ class HomeFragment : Fragment() {
private fun removeAllTabsAndShowSnackbar(sessionCode: String) {
val tabs = sessionManager.sessionsOfType(private = sessionCode == ALL_PRIVATE_TABS).toList()
val selectedIndex = sessionManager
.selectedSession?.let { sessionManager.sessions.indexOf(it) } ?: SessionManager.NO_SELECTION
.selectedSession?.let { sessionManager.sessions.indexOf(it) }
?: SessionManager.NO_SELECTION
val snapshot = tabs
.map(sessionManager::createSessionSnapshot)
@ -597,8 +598,8 @@ class HomeFragment : Fragment() {
}, owner = this@HomeFragment.viewLifecycleOwner)
}
if (context.settings().showPrivateModeContextualFeatureRecommender &&
browsingModeManager.mode.isPrivate
if (browsingModeManager.mode.isPrivate &&
context.settings().showPrivateModeCfr
) {
recommendPrivateBrowsingShortcut()
}
@ -683,8 +684,8 @@ class HomeFragment : Fragment() {
}
private fun recommendPrivateBrowsingShortcut() {
context?.let {
val layout = LayoutInflater.from(it)
context?.let { context ->
val layout = LayoutInflater.from(context)
.inflate(R.layout.pbm_shortcut_popup, null)
val privateBrowsingRecommend =
PopupWindow(
@ -712,6 +713,7 @@ class HomeFragment : Fragment() {
// We want to show the popup only after privateBrowsingButton is available.
// Otherwise, we will encounter an activity token error.
privateBrowsingButton.post {
context.settings().lastCfrShownTimeInMillis = System.currentTimeMillis()
privateBrowsingRecommend.showAsDropDown(
privateBrowsingButton, 0, CFR_Y_OFFSET, Gravity.TOP or Gravity.END
)
@ -736,15 +738,10 @@ class HomeFragment : Fragment() {
}
private fun navigateToSearch() {
val directions = if (FeatureFlags.newSearchExperience) {
val directions =
HomeFragmentDirections.actionGlobalSearchDialog(
sessionId = null
)
} else {
HomeFragmentDirections.actionGlobalSearch(
sessionId = null
)
}
nav(R.id.homeFragment, directions, getToolbarNavOptions(requireContext()))
}
@ -993,7 +990,6 @@ class HomeFragment : Fragment() {
const val ALL_PRIVATE_TABS = "all_private"
private const val FOCUS_ON_ADDRESS_BAR = "focusOnAddressBar"
private const val SESSION_TO_DELETE = "session_to_delete"
private const val ANIMATION_DELAY = 100L
private const val NON_TAB_ITEM_NUM = 3

@ -8,6 +8,11 @@ import android.os.Parcelable
import androidx.lifecycle.ViewModel
class HomeScreenViewModel : ViewModel() {
/**
* Used to delete a specific session once the home screen is resumed
*/
var sessionToDelete: String? = null
var layoutManagerState: Parcelable? = null
/**

@ -46,7 +46,7 @@ class StartSearchIntentProcessor(
out.removeExtra(HomeActivity.OPEN_TO_SEARCH)
val directions = source?.let {
NavGraphDirections.actionGlobalSearch(
NavGraphDirections.actionGlobalSearchDialog(
sessionId = null,
searchAccessPoint = it
)

@ -428,7 +428,7 @@ class DefaultSessionControlController(
}
override fun handlePaste(clipboardText: String) {
val directions = HomeFragmentDirections.actionGlobalSearch(
val directions = HomeFragmentDirections.actionGlobalSearchDialog(
sessionId = null,
pastedText = clipboardText
)

@ -94,7 +94,6 @@ private fun onboardingAdapterItems(onboardingState: OnboardingState): List<Adapt
val appName = it.getString(R.string.app_name)
it.getString(R.string.onboarding_feature_section_header, appName)
},
AdapterItem.OnboardingWhatsNew,
AdapterItem.OnboardingTrackingProtection,
AdapterItem.OnboardingThemePicker,
AdapterItem.OnboardingPrivateBrowsing,

@ -14,7 +14,7 @@ import kotlinx.android.synthetic.main.onboarding_automatic_signin.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import mozilla.components.service.fxa.manager.SignInWithShareableAccountResult
import mozilla.components.service.fxa.manager.MigrationResult
import mozilla.components.service.fxa.sharing.ShareableAccount
import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds
import org.mozilla.fenix.R
@ -56,8 +56,12 @@ class OnboardingAutomaticSignInViewHolder(
button.isEnabled = false
val accountManager = context.components.backgroundServices.accountManager
when (accountManager.signInWithShareableAccountAsync(shareableAccount).await()) {
SignInWithShareableAccountResult.Failure -> {
when (accountManager.migrateFromAccount(shareableAccount)) {
MigrationResult.WillRetry,
MigrationResult.Success -> {
// We consider both of these as a 'success'.
}
MigrationResult.Failure -> {
// Failed to sign-in (e.g. bad credentials). Allow to try again.
button.text = context.getString(R.string.onboarding_firefox_account_auto_signin_confirm)
button.isEnabled = true
@ -69,9 +73,6 @@ class OnboardingAutomaticSignInViewHolder(
context.getString(R.string.onboarding_firefox_account_automatic_signin_failed)
).show()
}
SignInWithShareableAccountResult.WillRetry, SignInWithShareableAccountResult.Success -> {
// We consider both of these as a 'success'.
}
}
}

@ -143,7 +143,7 @@ class DefaultBookmarkController(
scope.launch {
store.dispatch(BookmarkFragmentAction.StartSync)
invokePendingDeletion()
activity.components.backgroundServices.accountManager.syncNowAsync(SyncReason.User).await()
activity.components.backgroundServices.accountManager.syncNow(SyncReason.User)
// The current bookmark node we are viewing may be made invalid after syncing so we
// check if the current node is valid and if it isn't we find the nearest valid ancestor
// and open it

@ -6,6 +6,7 @@ package org.mozilla.fenix.library.bookmarks
import android.content.Context
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.mozilla.fenix.R
fun rootTitles(context: Context, withMobileRoot: Boolean): Map<String, String> = if (withMobileRoot) {
@ -35,3 +36,16 @@ fun friendlyRootTitle(
rootTitles.containsKey(node.title) -> rootTitles[node.title]
else -> node.title
}
data class BookmarkNodeWithDepth(val depth: Int, val node: BookmarkNode, val parent: String?)
fun BookmarkNode.flatNodeList(excludeSubtreeRoot: String?, depth: Int = 0): List<BookmarkNodeWithDepth> {
if (this.type != BookmarkNodeType.FOLDER || this.guid == excludeSubtreeRoot) {
return emptyList()
}
val newList = listOf(BookmarkNodeWithDepth(depth, this, this.parentGuid))
return newList + children
?.filter { it.type == BookmarkNodeType.FOLDER }
?.flatMap { it.flatNodeList(excludeSubtreeRoot = excludeSubtreeRoot, depth = depth + 1) }
.orEmpty()
}

@ -14,29 +14,23 @@ import androidx.recyclerview.widget.ListAdapter
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.R
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.bookmarks.BookmarkNodeWithDepth
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
import org.mozilla.fenix.library.bookmarks.flatNodeList
import org.mozilla.fenix.library.bookmarks.selectfolder.SelectBookmarkFolderAdapter.BookmarkFolderViewHolder
import org.mozilla.fenix.library.bookmarks.selectfolder.SelectBookmarkFolderAdapter.BookmarkNodeWithDepth
class SelectBookmarkFolderAdapter(private val sharedViewModel: BookmarksSharedViewModel) :
ListAdapter<BookmarkNodeWithDepth, BookmarkFolderViewHolder>(DiffCallback) {
fun updateData(tree: BookmarkNode?, hideFolderGuid: String?) {
val updatedData = tree
?.convertToFolderDepthTree()
?.flatNodeList(hideFolderGuid)
?.drop(1)
.orEmpty()
val filteredData = if (hideFolderGuid != null && updatedData.isNotEmpty()) {
updatedData.filter { it.node.guid != hideFolderGuid }
} else {
updatedData
}
submitList(filteredData)
submitList(updatedData)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkFolderViewHolder {
@ -101,16 +95,6 @@ class SelectBookmarkFolderAdapter(private val sharedViewModel: BookmarksSharedVi
}
}
data class BookmarkNodeWithDepth(val depth: Int, val node: BookmarkNode, val parent: String?)
private fun BookmarkNode.convertToFolderDepthTree(depth: Int = 0): List<BookmarkNodeWithDepth> {
val newList = listOf(BookmarkNodeWithDepth(depth, this, this.parentGuid))
return newList + children
?.filter { it.type == BookmarkNodeType.FOLDER }
?.flatMap { it.convertToFolderDepthTree(depth = depth + 1) }
.orEmpty()
}
private fun getSelectedItemIndex(): Int? {
val selectedNode = sharedViewModel.selectedFolder
val selectedNodeIndex = currentList.indexOfFirst { it.node == selectedNode }

@ -344,7 +344,7 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
private suspend fun syncHistory() {
val accountManager = requireComponents.backgroundServices.accountManager
accountManager.syncNowAsync(SyncReason.User).await()
accountManager.syncNow(SyncReason.User)
viewModel.invalidate()
}
}

@ -122,7 +122,7 @@ internal class OneTimeMessageDeliveryObserver(
authType: AuthType
) {
lazyAccount.value.withConstellation {
processRawEventAsync(String(message))
MainScope().launch { processRawEvent(String(message)) }
}
MainScope().launch {

@ -1,252 +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.search
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.text.SpannableString
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.navigation.NavController
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.support.ktx.kotlin.isUrl
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.Event.PerformedSearch.SearchAccessPoint.ACTION
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint.NONE
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint.SUGGESTION
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
import org.mozilla.fenix.crashes.CrashListActivity
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.SupportUtils.MozillaPage.MANIFESTO
import org.mozilla.fenix.utils.Settings
/**
* An interface that handles the view manipulation of the Search, triggered by the Interactor
*/
@Suppress("TooManyFunctions")
interface SearchController {
fun handleUrlCommitted(url: String)
fun handleEditingCancelled()
fun handleTextChanged(text: String)
fun handleUrlTapped(url: String)
fun handleSearchTermsTapped(searchTerms: String)
fun handleSearchShortcutEngineSelected(searchEngine: SearchEngine)
fun handleClickSearchEngineSettings()
fun handleExistingSessionSelected(session: Session)
fun handleExistingSessionSelected(tabId: String)
fun handleSearchShortcutsButtonClicked()
fun handleCameraPermissionsNeeded()
}
@Suppress("TooManyFunctions", "LongParameterList")
class DefaultSearchController(
private val activity: HomeActivity,
private val sessionManager: SessionManager,
private val store: SearchFragmentStore,
private val navController: NavController,
private val settings: Settings,
private val metrics: MetricController,
private val clearToolbarFocus: () -> Unit
) : SearchController {
override fun handleUrlCommitted(url: String) {
when (url) {
"about:crashes" -> {
// The list of past crashes can be accessed via "settings > about", but desktop and
// fennec users may be used to navigating to "about:crashes". So we intercept this here
// and open the crash list activity instead.
activity.startActivity(Intent(activity, CrashListActivity::class.java))
}
"about:addons" -> {
val directions = SearchFragmentDirections.actionGlobalAddonsManagementFragment()
navController.navigateSafe(R.id.searchFragment, directions)
}
"moz://a" -> openSearchOrUrl(SupportUtils.getMozillaPageUrl(MANIFESTO))
else -> if (url.isNotBlank()) {
openSearchOrUrl(url)
}
}
}
private fun openSearchOrUrl(url: String) {
activity.openToBrowserAndLoad(
searchTermOrURL = url,
newTab = store.state.tabId == null,
from = BrowserDirection.FromSearch,
engine = store.state.searchEngineSource.searchEngine
)
val event = if (url.isUrl()) {
Event.EnteredUrl(false)
} else {
settings.incrementActiveSearchCount()
val searchAccessPoint = when (store.state.searchAccessPoint) {
NONE -> ACTION
else -> store.state.searchAccessPoint
}
searchAccessPoint?.let { sap ->
MetricsUtils.createSearchEvent(
store.state.searchEngineSource.searchEngine,
activity,
sap
)
}
}
event?.let { metrics.track(it) }
}
override fun handleEditingCancelled() {
clearToolbarFocus()
}
override fun handleTextChanged(text: String) {
// Display the search shortcuts on each entry of the search fragment (see #5308)
val textMatchesCurrentUrl = store.state.url == text
val textMatchesCurrentSearch = store.state.searchTerms == text
store.dispatch(SearchFragmentAction.UpdateQuery(text))
store.dispatch(
SearchFragmentAction.ShowSearchShortcutEnginePicker(
(textMatchesCurrentUrl || textMatchesCurrentSearch || text.isEmpty()) &&
settings.shouldShowSearchShortcuts
)
)
store.dispatch(
SearchFragmentAction.AllowSearchSuggestionsInPrivateModePrompt(
text.isNotEmpty() &&
activity.browsingModeManager.mode.isPrivate &&
!settings.shouldShowSearchSuggestionsInPrivate &&
!settings.showSearchSuggestionsInPrivateOnboardingFinished
)
)
}
override fun handleUrlTapped(url: String) {
activity.openToBrowserAndLoad(
searchTermOrURL = url,
newTab = store.state.tabId == null,
from = BrowserDirection.FromSearch
)
metrics.track(Event.EnteredUrl(false))
}
override fun handleSearchTermsTapped(searchTerms: String) {
settings.incrementActiveSearchCount()
activity.openToBrowserAndLoad(
searchTermOrURL = searchTerms,
newTab = store.state.tabId == null,
from = BrowserDirection.FromSearch,
engine = store.state.searchEngineSource.searchEngine,
forceSearch = true
)
val searchAccessPoint = when (store.state.searchAccessPoint) {
NONE -> SUGGESTION
else -> store.state.searchAccessPoint
}
val event = searchAccessPoint?.let { sap ->
MetricsUtils.createSearchEvent(
store.state.searchEngineSource.searchEngine,
activity,
sap
)
}
event?.let { metrics.track(it) }
}
override fun handleSearchShortcutEngineSelected(searchEngine: SearchEngine) {
store.dispatch(SearchFragmentAction.SearchShortcutEngineSelected(searchEngine))
val isCustom =
CustomSearchEngineStore.isCustomSearchEngine(activity, searchEngine.identifier)
metrics.track(Event.SearchShortcutSelected(searchEngine, isCustom))
}
override fun handleSearchShortcutsButtonClicked() {
val isOpen = store.state.showSearchShortcuts
store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(!isOpen))
}
override fun handleClickSearchEngineSettings() {
val directions = SearchFragmentDirections.actionGlobalSearchEngineFragment()
navController.navigateSafe(R.id.searchFragment, directions)
}
override fun handleExistingSessionSelected(session: Session) {
sessionManager.select(session)
activity.openToBrowser(
from = BrowserDirection.FromSearch
)
}
override fun handleExistingSessionSelected(tabId: String) {
val session = sessionManager.findSessionById(tabId)
if (session != null) {
handleExistingSessionSelected(session)
}
}
/**
* Creates and shows an [AlertDialog] when camera permissions are needed.
*
* In versions above M, [AlertDialog.BUTTON_POSITIVE] takes the user to the app settings. This
* intent only exists in M and above. Below M, [AlertDialog.BUTTON_POSITIVE] routes to a SUMO
* help page to find the app settings.
*
* [AlertDialog.BUTTON_NEGATIVE] dismisses the dialog.
*/
override fun handleCameraPermissionsNeeded() {
val dialog = buildDialog()
dialog.show()
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun buildDialog(): AlertDialog.Builder {
return AlertDialog.Builder(activity).apply {
val spannableText = SpannableString(
activity.resources.getString(R.string.camera_permissions_needed_message)
)
setMessage(spannableText)
setNegativeButton(R.string.camera_permissions_needed_negative_button_text) {
dialog: DialogInterface, _ ->
dialog.cancel()
}
setPositiveButton(R.string.camera_permissions_needed_positive_button_text) {
dialog: DialogInterface, _ ->
val intent: Intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
} else {
SupportUtils.createCustomTabIntent(
activity,
SupportUtils.getSumoURLForTopic(
activity,
SupportUtils.SumoTopic.QR_CAMERA_ACCESS
)
)
}
val uri = Uri.fromParts("package", activity.packageName, null)
intent.data = uri
dialog.cancel()
activity.startActivity(intent)
}
create()
}
}
}

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.searchdialog
package org.mozilla.fenix.search
import android.content.DialogInterface
import android.content.Intent
@ -25,12 +25,27 @@ import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
import org.mozilla.fenix.crashes.CrashListActivity
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.search.SearchController
import org.mozilla.fenix.search.SearchFragmentAction
import org.mozilla.fenix.search.SearchFragmentStore
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Settings
/**
* An interface that handles the view manipulation of the Search, triggered by the Interactor
*/
@Suppress("TooManyFunctions")
interface SearchController {
fun handleUrlCommitted(url: String)
fun handleEditingCancelled()
fun handleTextChanged(text: String)
fun handleUrlTapped(url: String)
fun handleSearchTermsTapped(searchTerms: String)
fun handleSearchShortcutEngineSelected(searchEngine: SearchEngine)
fun handleClickSearchEngineSettings()
fun handleExistingSessionSelected(session: Session)
fun handleExistingSessionSelected(tabId: String)
fun handleSearchShortcutsButtonClicked()
fun handleCameraPermissionsNeeded()
}
@Suppress("TooManyFunctions", "LongParameterList")
class SearchDialogController(
private val activity: HomeActivity,

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.searchdialog
package org.mozilla.fenix.search
import android.Manifest
import android.app.Activity
@ -56,19 +56,13 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.isKeyboardVisible
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.search.SearchFragmentAction
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.settings.SupportUtils
import org.mozilla.fenix.settings.registerOnSharedPreferenceChangeListener
import org.mozilla.fenix.widget.VoiceSearchActivity
typealias SearchDialogFragmentStore = SearchFragmentStore
typealias SearchDialogInteractor = SearchInteractor
@SuppressWarnings("LargeClass", "TooManyFunctions")
class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {

@ -14,8 +14,8 @@ import org.mozilla.fenix.search.toolbar.ToolbarInteractor
* Provides implementations for the AwesomeBarView and ToolbarView
*/
@Suppress("TooManyFunctions")
class SearchInteractor(
private val searchController: SearchController
class SearchDialogInteractor(
private val searchController: SearchDialogController
) : AwesomeBarInteractor, ToolbarInteractor {
override fun onUrlCommitted(url: String) {

@ -1,462 +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.search
import android.Manifest
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Typeface.BOLD
import android.graphics.Typeface.ITALIC
import android.os.Bundle
import android.speech.RecognizerIntent
import android.speech.RecognizerIntent.EXTRA_RESULTS
import android.text.style.StyleSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewStub
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.android.synthetic.main.fragment_search.view.*
import kotlinx.android.synthetic.main.search_suggestions_hint.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.qr.QrFeature
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.content.hasCamera
import mozilla.components.support.ktx.android.content.isPermissionGranted
import mozilla.components.support.ktx.android.content.res.getSpanned
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.ui.autocomplete.InlineAutocompleteEditText
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.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
import org.mozilla.fenix.widget.VoiceSearchActivity.Companion.SPEECH_REQUEST_CODE
@Suppress("TooManyFunctions", "LargeClass")
class SearchFragment : Fragment(), UserInteractionHandler {
private lateinit var toolbarView: ToolbarView
private lateinit var awesomeBarView: AwesomeBarView
private val qrFeature = ViewBoundFeatureWrapper<QrFeature>()
private lateinit var searchStore: SearchFragmentStore
private lateinit var searchInteractor: SearchInteractor
private val speechIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
@Suppress("LongMethod")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val activity = activity as HomeActivity
val settings = activity.settings()
val args by navArgs<SearchFragmentArgs>()
val view = inflater.inflate(R.layout.fragment_search, container, false)
val isPrivate = activity.browsingModeManager.mode.isPrivate
requireComponents.analytics.metrics.track(Event.InteractWithSearchURLArea)
searchStore = StoreProvider.get(this) {
SearchFragmentStore(
createInitialSearchFragmentState(
activity,
requireComponents,
tabId = args.sessionId,
pastedText = args.pastedText,
searchAccessPoint = args.searchAccessPoint
)
)
}
val searchController = DefaultSearchController(
activity = activity,
sessionManager = requireComponents.core.sessionManager,
store = searchStore,
navController = findNavController(),
settings = settings,
metrics = requireComponents.analytics.metrics,
clearToolbarFocus = ::clearToolbarFocus
)
searchInteractor = SearchInteractor(
searchController
)
awesomeBarView = AwesomeBarView(
activity,
searchInteractor,
view.findViewById(R.id.awesomeBar)
)
setShortcutsChangedListener(CustomSearchEngineStore.PREF_FILE_SEARCH_ENGINES)
setShortcutsChangedListener(FenixSearchEngineProvider.PREF_FILE_SEARCH_ENGINES)
view.scrollView.setOnScrollChangeListener {
_: NestedScrollView, _: Int, _: Int, _: Int, _: Int ->
view.hideKeyboard()
}
toolbarView = ToolbarView(
requireContext(),
searchInteractor,
historyStorageProvider(),
isPrivate,
view.toolbar,
requireComponents.core.engine
)
toolbarView.view.addEditAction(
BrowserToolbar.Button(
ContextCompat.getDrawable(requireContext(), R.drawable.ic_microphone)!!,
requireContext().getString(R.string.voice_search_content_description),
visible = {
searchStore.state.searchEngineSource.searchEngine.identifier.contains("google") &&
speechIsAvailable() &&
settings.shouldShowVoiceSearch
},
listener = ::launchVoiceSearch
)
)
awesomeBarView.view.setOnEditSuggestionListener(toolbarView.view::setSearchTerms)
val urlView = toolbarView.view
.findViewById<InlineAutocompleteEditText>(R.id.mozac_browser_toolbar_edit_url_view)
urlView?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
requireComponents.core.engine.speculativeCreateSession(isPrivate)
startPostponedEnterTransition()
return view
}
private fun speechIsAvailable(): Boolean {
return (speechIntent.resolveActivity(requireContext().packageManager) != null)
}
private fun setShortcutsChangedListener(preferenceFileName: String) {
requireContext().getSharedPreferences(
preferenceFileName,
Context.MODE_PRIVATE
).registerOnSharedPreferenceChangeListener(viewLifecycleOwner) { _, _ ->
awesomeBarView.update(searchStore.state)
}
}
private fun launchVoiceSearch() {
// Note if a user disables speech while the app is on the search fragment
// the voice button will still be available and *will* cause a crash if tapped,
// since the `visible` call is only checked on create. In order to avoid extra complexity
// around such a small edge case, we make the button have no functionality in this case.
if (!speechIsAvailable()) { return }
requireComponents.analytics.metrics.track(Event.VoiceSearchTapped)
speechIntent.apply {
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
putExtra(RecognizerIntent.EXTRA_PROMPT, requireContext().getString(R.string.voice_search_explainer))
}
startActivityForResult(speechIntent, SPEECH_REQUEST_CODE)
}
private fun clearToolbarFocus() {
toolbarView.view.hideKeyboard()
toolbarView.view.clearFocus()
}
@ExperimentalCoroutinesApi
@SuppressWarnings("LongMethod")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
search_scan_button.visibility = if (context?.hasCamera() == true) View.VISIBLE else View.GONE
qrFeature.set(
createQrFeature(),
owner = this,
view = view
)
view.search_scan_button.setOnClickListener {
if (requireContext().settings().shouldShowCameraPermissionPrompt) {
requireComponents.analytics.metrics.track(Event.QRScannerOpened)
qrFeature.get()?.scan(R.id.container)
} else {
if (requireContext().isPermissionGranted(Manifest.permission.CAMERA)) {
requireComponents.analytics.metrics.track(Event.QRScannerOpened)
qrFeature.get()?.scan(R.id.container)
} else {
searchInteractor.onCameraPermissionsNeeded()
}
}
view.hideKeyboard()
search_scan_button.isChecked = false
requireContext().settings().setCameraPermissionNeededState = false
}
view.search_engines_shortcut_button.setOnClickListener {
searchInteractor.onSearchShortcutsButtonClicked()
}
val stubListener = ViewStub.OnInflateListener { _, inflated ->
inflated.learn_more.setOnClickListener {
(activity as HomeActivity)
.openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getGenericSumoURLForTopic(
SupportUtils.SumoTopic.SEARCH_SUGGESTION
),
newTab = searchStore.state.tabId == null,
from = BrowserDirection.FromSearch
)
}
inflated.allow.setOnClickListener {
inflated.visibility = View.GONE
context?.settings()?.shouldShowSearchSuggestionsInPrivate = true
context?.settings()?.showSearchSuggestionsInPrivateOnboardingFinished = true
searchStore.dispatch(SearchFragmentAction.SetShowSearchSuggestions(true))
searchStore.dispatch(SearchFragmentAction.AllowSearchSuggestionsInPrivateModePrompt(false))
requireComponents.analytics.metrics.track(Event.PrivateBrowsingShowSearchSuggestions)
}
inflated.dismiss.setOnClickListener {
inflated.visibility = View.GONE
context?.settings()?.shouldShowSearchSuggestionsInPrivate = false
context?.settings()?.showSearchSuggestionsInPrivateOnboardingFinished = true
}
inflated.text.text =
getString(R.string.search_suggestions_onboarding_text, getString(R.string.app_name))
inflated.title.text =
getString(R.string.search_suggestions_onboarding_title)
}
view.search_suggestions_onboarding.setOnInflateListener((stubListener))
fill_link_from_clipboard.setOnClickListener {
(activity as HomeActivity)
.openToBrowserAndLoad(
searchTermOrURL = requireContext().components.clipboardHandler.url ?: "",
newTab = searchStore.state.tabId == null,
from = BrowserDirection.FromSearch
)
}
consumeFrom(searchStore) {
awesomeBarView.update(it)
updateSearchShortcutsIcon(it)
toolbarView.update(it)
updateSearchWithLabel(it)
updateClipboardSuggestion(it, requireContext().components.clipboardHandler.url)
updateSearchSuggestionsHintVisibility(it)
updateToolbarContentDescription(it)
}
startPostponedEnterTransition()
}
private fun createQrFeature(): QrFeature {
return QrFeature(
requireContext(),
fragmentManager = parentFragmentManager,
onNeedToRequestPermissions = { permissions ->
requestPermissions(permissions, REQUEST_CODE_CAMERA_PERMISSIONS)
},
onScanResult = { result ->
search_scan_button.isChecked = false
activity?.let {
AlertDialog.Builder(it).apply {
val spannable = resources.getSpanned(
R.string.qr_scanner_confirmation_dialog_message,
getString(R.string.app_name) to StyleSpan(BOLD),
result to StyleSpan(ITALIC)
)
setMessage(spannable)
setNegativeButton(R.string.qr_scanner_dialog_negative) { dialog: DialogInterface, _ ->
requireComponents.analytics.metrics.track(Event.QRScannerNavigationDenied)
dialog.cancel()
resetFocus()
}
setPositiveButton(R.string.qr_scanner_dialog_positive) { dialog: DialogInterface, _ ->
requireComponents.analytics.metrics.track(Event.QRScannerNavigationAllowed)
(activity as HomeActivity)
.openToBrowserAndLoad(
searchTermOrURL = result,
newTab = searchStore.state.tabId == null,
from = BrowserDirection.FromSearch
)
dialog.dismiss()
resetFocus()
}
create()
}.show()
requireComponents.analytics.metrics.track(Event.QRScannerPromptDisplayed)
}
}
)
}
private fun updateToolbarContentDescription(searchState: SearchFragmentState) {
val urlView = toolbarView.view
.findViewById<InlineAutocompleteEditText>(R.id.mozac_browser_toolbar_edit_url_view)
toolbarView.view.contentDescription =
searchState.searchEngineSource.searchEngine.name + ", " + urlView.hint
urlView?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
}
override fun onResume() {
super.onResume()
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 = provider.getDefaultEngine(requireContext())
if (searchStore.state.defaultEngineSource.searchEngine != currentDefaultEngine) {
searchStore.dispatch(
SearchFragmentAction.SelectNewDefaultSearchEngine
(currentDefaultEngine)
)
}
// Users can from this fragment go to install/uninstall search engines and then return.
val areShortcutsAvailable = provider.areShortcutsAvailable(requireContext())
if (searchStore.state.areShortcutsAvailable != areShortcutsAvailable) {
searchStore.dispatch(SearchFragmentAction.UpdateShortcutsAvailability(areShortcutsAvailable))
}
updateClipboardSuggestion(
searchStore.state,
requireComponents.clipboardHandler.url
)
hideToolbar()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
if (requestCode == SPEECH_REQUEST_CODE && resultCode == RESULT_OK) {
intent?.getStringArrayListExtra(EXTRA_RESULTS)?.first()?.also {
toolbarView.view.edit.updateUrl(url = it, shouldHighlight = true)
searchInteractor.onTextChanged(it)
toolbarView.view.edit.focus()
}
}
}
override fun onPause() {
super.onPause()
toolbarView.view.clearFocus()
}
override fun onBackPressed(): Boolean {
return when {
qrFeature.onBackPressed() -> {
resetFocus()
true
}
else -> false
}
}
private fun resetFocus() {
search_scan_button.isChecked = false
toolbarView.view.edit.focus()
toolbarView.view.requestFocus()
}
private fun updateSearchWithLabel(searchState: SearchFragmentState) {
search_engine_shortcut.visibility =
if (searchState.showSearchShortcuts) View.VISIBLE else View.GONE
}
private fun updateClipboardSuggestion(searchState: SearchFragmentState, clipboardUrl: String?) {
val visibility =
if (searchState.showClipboardSuggestions && searchState.query.isEmpty() && !clipboardUrl.isNullOrEmpty())
View.VISIBLE else View.GONE
fill_link_from_clipboard.visibility = visibility
divider_line.visibility = visibility
clipboard_url.text = clipboardUrl
if (clipboardUrl != null && !((activity as HomeActivity).browsingModeManager.mode.isPrivate)) {
requireComponents.core.engine.speculativeConnect(clipboardUrl)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
when (requestCode) {
REQUEST_CODE_CAMERA_PERMISSIONS -> qrFeature.withFeature {
it.onPermissionsResult(permissions, grantResults)
resetFocus()
requireContext().settings().setCameraPermissionNeededState = false
}
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
private fun historyStorageProvider(): HistoryStorage? {
return if (requireContext().settings().shouldShowHistorySuggestions) {
requireComponents.core.historyStorage
} else null
}
private fun updateSearchSuggestionsHintVisibility(state: SearchFragmentState) {
view?.apply {
findViewById<View>(R.id.search_suggestions_onboarding)?.isVisible = state.showSearchSuggestionsHint
search_suggestions_onboarding_divider?.isVisible =
search_engine_shortcut.isVisible && state.showSearchSuggestionsHint
}
}
private fun updateSearchShortcutsIcon(searchState: SearchFragmentState) {
view?.apply {
search_engines_shortcut_button.isVisible = searchState.areShortcutsAvailable
val showShortcuts = searchState.showSearchShortcuts
search_engines_shortcut_button.isChecked = showShortcuts
val color = if (showShortcuts) R.attr.contrastText else R.attr.primaryText
search_engines_shortcut_button.compoundDrawables[0]?.setTint(
requireContext().getColorFromAttr(color)
)
}
}
companion object {
private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1
}
}

@ -130,7 +130,7 @@ class AwesomeBarView(
syncedTabsStorageSuggestionProvider =
SyncedTabsStorageSuggestionProvider(
components.backgroundServices.syncedTabsStorage,
components.useCases.tabsUseCases.addTab,
loadUrlUseCase,
components.core.icons,
DeviceIndicators(
getDrawable(activity, R.drawable.ic_search_results_device_desktop),

@ -321,6 +321,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
val preferenceRemoteDebugging = findPreference<Preference>(debuggingKey)
val preferenceMakeDefaultBrowser =
requirePreference<Preference>(R.string.pref_key_make_default_browser)
val preferenceOpenLinksInExternalApp =
findPreference<Preference>(getPreferenceKey(R.string.pref_key_open_links_in_external_app))
preferencePrivateBrowsing.icon.mutate().apply {
setTint(requireContext().getColorFromAttr(R.attr.primaryText))
@ -345,6 +347,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
preferenceMakeDefaultBrowser.onPreferenceClickListener =
getClickListenerForMakeDefaultBrowser()
preferenceOpenLinksInExternalApp?.onPreferenceChangeListener = SharedPreferenceUpdater()
val preferenceFxAOverride =
findPreference<Preference>(getPreferenceKey(R.string.pref_key_override_fxa_server))
val preferenceSyncOverride =

@ -169,25 +169,32 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
updateSyncEngineStates()
setDisabledWhileSyncing(accountManager.isSyncActive())
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_history).apply {
setOnPreferenceChangeListener { _, newValue ->
SyncEnginesStorage(context).setStatus(SyncEngine.History, newValue as Boolean)
@Suppress("DeferredResultUnused")
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
true
fun updateSyncEngineState(context: Context, engine: SyncEngine, newState: Boolean) {
SyncEnginesStorage(context).setStatus(engine, newState)
viewLifecycleOwner.lifecycleScope.launch {
context.components.backgroundServices.accountManager.syncNow(SyncReason.EngineChange)
}
}
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_bookmarks).apply {
setOnPreferenceChangeListener { _, newValue ->
SyncEnginesStorage(context).setStatus(SyncEngine.Bookmarks, newValue as Boolean)
@Suppress("DeferredResultUnused")
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
true
fun SyncEngine.prefId(): Int = when (this) {
SyncEngine.History -> R.string.pref_key_sync_history
SyncEngine.Bookmarks -> R.string.pref_key_sync_bookmarks
SyncEngine.Passwords -> R.string.pref_key_sync_logins
SyncEngine.Tabs -> R.string.pref_key_sync_tabs
else -> throw IllegalStateException("Accessing internal sync engines")
}
listOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Tabs).forEach {
requirePreference<CheckBoxPreference>(it.prefId()).apply {
setOnPreferenceChangeListener { _, newValue ->
updateSyncEngineState(context, it, newValue as Boolean)
true
}
}
}
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_logins).apply {
// 'Passwords' listener is special, since we also display a pin protection warning.
requirePreference<CheckBoxPreference>(SyncEngine.Passwords.prefId()).apply {
setOnPreferenceChangeListener { _, newValue ->
val manager =
activity?.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
@ -195,9 +202,7 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
newValue == false ||
!context.settings().shouldShowSecurityPinWarningSync
) {
SyncEnginesStorage(context).setStatus(SyncEngine.Passwords, newValue as Boolean)
@Suppress("DeferredResultUnused")
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
updateSyncEngineState(context, SyncEngine.Passwords, newValue as Boolean)
} else {
showPinDialogWarning(newValue as Boolean)
}
@ -205,15 +210,6 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
}
}
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_tabs).apply {
setOnPreferenceChangeListener { _, newValue ->
SyncEnginesStorage(context).setStatus(SyncEngine.Tabs, newValue as Boolean)
@Suppress("DeferredResultUnused")
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
true
}
}
deviceConstellation?.registerDeviceObserver(
deviceConstellationObserver,
owner = this,
@ -237,8 +233,9 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
setNegativeButton(getString(R.string.logins_warning_dialog_later)) { _: DialogInterface, _ ->
SyncEnginesStorage(context).setStatus(SyncEngine.Passwords, newValue)
@Suppress("DeferredResultUnused")
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
viewLifecycleOwner.lifecycleScope.launch {
context.components.backgroundServices.accountManager.syncNow(SyncReason.EngineChange)
}
}
setPositiveButton(getString(R.string.logins_warning_dialog_set_up_now)) { it: DialogInterface, _ ->
@ -278,13 +275,12 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
viewLifecycleOwner.lifecycleScope.launch {
requireComponents.analytics.metrics.track(Event.SyncAccountSyncNow)
// Trigger a sync.
requireComponents.backgroundServices.accountManager.syncNowAsync(SyncReason.User)
.await()
requireComponents.backgroundServices.accountManager.syncNow(SyncReason.User)
// Poll for device events & update devices.
accountManager.authenticatedAccount()
?.deviceConstellation()?.run {
refreshDevicesAsync().await()
pollForCommandsAsync().await()
refreshDevices()
pollForCommands()
}
}
}
@ -298,8 +294,7 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
context?.let {
accountManager.authenticatedAccount()
?.deviceConstellation()
?.setDeviceNameAsync(newValue, it)
?.await()
?.setDeviceName(newValue, it)
}
}
return true

@ -64,7 +64,7 @@ class SignOutFragment : BottomSheetDialogFragment() {
viewLifecycleOwner.lifecycleScope.launch {
requireComponents
.backgroundServices.accountAbnormalities.userRequestedLogout()
accountManager.logoutAsync().await()
accountManager.logout()
}.invokeOnCompletion {
if (!findNavController().popBackStack(R.id.settingsFragment, false)) {
dismiss()

@ -6,7 +6,6 @@ package org.mozilla.fenix.settings.account
import android.Manifest
import android.os.Bundle
import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -26,7 +25,6 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.addUnderline
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
@ -72,6 +70,7 @@ class TurnOnSyncFragment : Fragment(), AccountObserver {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requireComponents.backgroundServices.accountManager.register(this, owner = this)
requireComponents.analytics.metrics.track(Event.SyncAuthOpened)
// App can be installed on devices with no camera modules. Like Android TV boxes.
@ -125,17 +124,10 @@ class TurnOnSyncFragment : Fragment(), AccountObserver {
DefaultSyncController(activity = activity as HomeActivity)
)
val createAccountActionText = getString(R.string.sign_in_create_account_link)
val fullText = getString(R.string.sign_in_create_account_text, createAccountActionText)
val spanStart = fullText.indexOf(createAccountActionText, 0, false)
val spanEnd = spanStart + createAccountActionText.length
view.createAccount.apply {
text = fullText
addUnderline(
spanStart,
spanEnd,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
text = HtmlCompat.fromHtml(
getString(R.string.sign_in_create_account_text),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
setOnClickListener(createAccountClickListener)
}
@ -143,28 +135,23 @@ class TurnOnSyncFragment : Fragment(), AccountObserver {
}
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
// If we're in a `shouldLoginJustWithEmail = true` state, we won't have a view available,
// and can't display a snackbar.
if (view == null) {
return
}
val snackbarText = requireContext().getString(R.string.sync_syncing_in_progress)
val snackbarLength = FenixSnackbar.LENGTH_SHORT
// Since the snackbar can be presented in BrowserFragment or in SettingsFragment we must
// base our display method on the padSnackbar argument
if (args.padSnackbar) {
FenixSnackbar.make(
view = requireView(),
duration = snackbarLength,
isDisplayedWithBrowserToolbar = true
)
.setText(snackbarText)
.show()
} else {
FenixSnackbar.make(
view = requireView(),
duration = snackbarLength,
isDisplayedWithBrowserToolbar = false
)
.setText(snackbarText)
.show()
}
FenixSnackbar.make(
view = requireView(),
duration = snackbarLength,
isDisplayedWithBrowserToolbar = args.padSnackbar
)
.setText(snackbarText)
.show()
}
private fun navigateToPairWithEmail() {

@ -7,6 +7,8 @@ package org.mozilla.fenix.settings.logins
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController
import androidx.preference.Preference
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
@ -28,10 +30,15 @@ class SyncLoginsPreferenceView(
init {
accountManager.register(object : AccountObserver {
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) =
updateSyncPreferenceStatus()
override fun onLoggedOut() = updateSyncPreferenceNeedsLogin()
override fun onAuthenticationProblems() = updateSyncPreferenceNeedsReauth()
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
MainScope().launch { updateSyncPreferenceStatus() }
}
override fun onLoggedOut() {
MainScope().launch { updateSyncPreferenceNeedsLogin() }
}
override fun onAuthenticationProblems() {
MainScope().launch { updateSyncPreferenceNeedsReauth() }
}
}, owner = lifecycleOwner)
val accountExists = accountManager.authenticatedAccount() != null

@ -4,6 +4,7 @@
package org.mozilla.fenix.settings.logins.fragment
import android.content.res.ColorStateList
import android.os.Bundle
import android.text.Editable
import android.text.InputType
@ -13,6 +14,8 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.view.menu.ActionMenuItemView
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
@ -114,6 +117,8 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
usernameChanged = false
passwordChanged = false
clearUsernameTextButton.isEnabled = oldLogin.username.isNotEmpty()
}
private fun formatEditableValues() {
@ -140,7 +145,6 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
passwordText.isCursorVisible = true
passwordText.hasFocus()
inputLayoutPassword.hasFocus()
it.isEnabled = false
}
revealPasswordButton.setOnClickListener {
togglePasswordReveal(passwordText, revealPasswordButton)
@ -168,13 +172,14 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
validUsername = true
inputLayoutUsername.error = null
inputLayoutUsername.errorIconDrawable = null
clearUsernameTextButton.isVisible = true
}
else -> {
usernameChanged = true
clearUsernameTextButton.isEnabled = true
setDupeError()
}
}
clearUsernameTextButton.isEnabled = u.toString().isNotEmpty()
setSaveButtonState()
}
@ -192,7 +197,8 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
when {
p.toString().isEmpty() -> {
passwordChanged = true
clearPasswordTextButton.isEnabled = false
revealPasswordButton.isVisible = false
clearPasswordTextButton.isVisible = false
setPasswordError()
}
p.toString() == oldLogin.password -> {
@ -200,14 +206,16 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
validPassword = true
inputLayoutPassword.error = null
inputLayoutPassword.errorIconDrawable = null
clearPasswordTextButton.isEnabled = true
revealPasswordButton.isVisible = true
clearPasswordTextButton.isVisible = true
}
else -> {
passwordChanged = true
validPassword = true
inputLayoutPassword.error = null
inputLayoutPassword.errorIconDrawable = null
clearPasswordTextButton.isEnabled = true
revealPasswordButton.isVisible = true
clearPasswordTextButton.isVisible = true
}
}
setSaveButtonState()
@ -231,13 +239,21 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
inputLayoutUsername?.let {
usernameChanged = true
validUsername = false
it.setErrorIconDrawable(R.drawable.mozac_ic_warning)
it.error = context?.getString(R.string.saved_login_duplicate)
it.setErrorIconDrawable(R.drawable.mozac_ic_warning_with_bottom_padding)
it.setErrorIconTintList(
ColorStateList.valueOf(
ContextCompat.getColor(requireContext(), R.color.design_error)
)
)
clearUsernameTextButton.isVisible = false
}
} else {
usernameChanged = true
validUsername = true
inputLayoutUsername.error = null
inputLayoutUsername.errorIconDrawable = null
clearUsernameTextButton.isVisible = true
}
}
@ -245,7 +261,12 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login) {
inputLayoutPassword?.let { layout ->
validPassword = false
layout.error = context?.getString(R.string.saved_login_password_required)
layout.setErrorIconDrawable(R.drawable.mozac_ic_warning)
layout.setErrorIconDrawable(R.drawable.mozac_ic_warning_with_bottom_padding)
layout.setErrorIconTintList(
ColorStateList.valueOf(
ContextCompat.getColor(requireContext(), R.color.design_error)
)
)
}
}

@ -54,8 +54,7 @@ class ShareViewModel(application: Application) : AndroidViewModel(application) {
viewModelScope.launch(ioDispatcher) {
fxaAccountManager.authenticatedAccount()
?.deviceConstellation()
?.refreshDevicesAsync()
?.await()
?.refreshDevices()
val devicesShareOptions = buildDeviceList(fxaAccountManager, network)
devicesListLiveData.postValue(devicesShareOptions)

@ -24,10 +24,11 @@ class PwaOnboardingObserver(
override fun onLoadingStateChanged(session: Session, loading: Boolean) {
if (!loading && webAppUseCases.isInstallable() && !settings.userKnowsAboutPwas) {
settings.incrementVisitedInstallableCount()
if (settings.shouldShowPwaOnboarding) {
if (settings.shouldShowPwaCfr) {
val directions =
BrowserFragmentDirections.actionBrowserFragmentToPwaOnboardingDialogFragment()
navController.nav(R.id.browserFragment, directions)
settings.lastCfrShownTimeInMillis = System.currentTimeMillis()
settings.userKnowsAboutPwas = true
}
}

@ -43,6 +43,7 @@ interface TabTrayController {
fun handleOpenTab(tab: Tab)
fun handleEnterMultiselect()
fun handleRecentlyClosedClicked()
fun handleSetUpAutoCloseTabsClicked()
}
/**
@ -184,4 +185,9 @@ class DefaultTabTrayController(
val directions = TabTrayDialogFragmentDirections.actionGlobalRecentlyClosed()
navController.navigate(directions)
}
override fun handleSetUpAutoCloseTabsClicked() {
val directions = TabTrayDialogFragmentDirections.actionGlobalCloseTabSettingsFragment()
navController.navigate(directions)
}
}

@ -16,6 +16,8 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@ -41,8 +43,8 @@ import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.view.showKeyboard
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.TabCollectionStorage
@ -53,6 +55,7 @@ import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.normalSessionSize
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentState.Mode
import org.mozilla.fenix.utils.allowUndo
@ -269,7 +272,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
val session = sessionManager.findSessionById(sessionId) ?: return
// Check if this is the last tab of this session type
val isLastOpenTab = store.state.tabs.filter { it.content.private == tab.content.private }.size == 1
val isLastOpenTab =
store.state.tabs.filter { it.content.private == tab.content.private }.size == 1
if (isLastOpenTab) {
dismissTabTrayAndNavigateHome(sessionId)
return
@ -288,7 +292,11 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
snackbarMessage,
getString(R.string.snackbar_deleted_undo),
{
sessionManager.add(session, isSelected, engineSessionState = tab.engineState.engineSessionState)
sessionManager.add(
session,
isSelected,
engineSessionState = tab.engineState.engineSessionState
)
_tabTrayView?.scrollToTab(session.id)
},
operation = { },
@ -297,8 +305,13 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
)
}
private val homeViewModel: HomeScreenViewModel by activityViewModels {
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
}
private fun dismissTabTrayAndNavigateHome(sessionId: String) {
val directions = BrowserFragmentDirections.actionGlobalHome(sessionToDelete = sessionId)
homeViewModel.sessionToDelete = sessionId
val directions = NavGraphDirections.actionGlobalHome()
findNavController().navigate(directions)
dismissAllowingStateLoss()
}

@ -54,6 +54,11 @@ interface TabTrayInteractor {
*/
fun onModeRequested(): TabTrayDialogFragmentState.Mode
/**
* Called when user clicks on the "set it up" prompt for automatically closing tabs
*/
fun onSetUpAutoCloseTabsClicked()
/**
* Called when a tab should be opened in the browser.
*/
@ -140,4 +145,8 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab
override fun onEnterMultiselect() {
controller.handleEnterMultiselect()
}
override fun onSetUpAutoCloseTabsClicked() {
controller.handleSetUpAutoCloseTabsClicked()
}
}

@ -27,10 +27,11 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayout
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_tabs_screen_top.view.*
import kotlinx.android.synthetic.main.component_tabs_screen_top.view.exit_tabs_screen
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.collect_multi_select
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.exit_multi_select
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.handle
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.infoBanner
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.multiselect_title
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tab_layout
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tab_tray_empty_view
@ -39,8 +40,8 @@ import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tab_tray_ov
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tab_wrapper
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.tabsTray
import kotlinx.android.synthetic.main.component_tabstray_bottom.view.topBar
import kotlinx.android.synthetic.main.component_tabstray_fab_bottom.view.*
import kotlinx.android.synthetic.main.tabs_tray_tab_counter.*
import kotlinx.android.synthetic.main.component_tabstray_fab_bottom.view.new_tab_button
import kotlinx.android.synthetic.main.tabs_tray_tab_counter.counter_text
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -55,6 +56,7 @@ import mozilla.components.browser.tabstray.TabViewHolder
import mozilla.components.feature.syncedtabs.SyncedTabsFeature
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.InfoBanner
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
@ -314,7 +316,29 @@ class TabTrayView(
}
}
adjustNewTabButtonForNormalMode()
adjustNewTabButtonsForNormalMode()
if (
view.context.settings().shouldShowAutoCloseTabsBanner &&
view.context.settings().canShowCfr &&
tabs.size >= TAB_COUNT_SHOW_CFR
) {
InfoBanner(
context = view.context,
message = view.context.getString(R.string.tab_tray_close_tabs_banner_message),
dismissText = view.context.getString(R.string.tab_tray_close_tabs_banner_negative_button_text),
actionText = view.context.getString(R.string.tab_tray_close_tabs_banner_positive_button_text),
container = view.infoBanner,
dismissByHiding = true,
dismissAction = { view.context.settings().shouldShowAutoCloseTabsBanner = false }
) {
interactor.onSetUpAutoCloseTabsClicked()
view.context.settings().shouldShowAutoCloseTabsBanner = false
}.apply {
view.infoBanner.visibility = View.VISIBLE
showBanner()
}
}
}
private fun gridViewNumberOfCols(context: Context): Int {
@ -329,9 +353,9 @@ class TabTrayView(
interactor.onSyncedTabClicked(tab)
}
private fun adjustNewTabButtonForNormalMode() {
private fun adjustNewTabButtonsForNormalMode() {
view.tab_tray_new_tab.apply {
visibility = if (useFab) View.GONE else View.VISIBLE
isVisible = !useFab
setOnClickListener {
sendNewTabEvent(isPrivateModeSelected)
interactor.onNewTabTapped(isPrivateModeSelected)
@ -552,7 +576,7 @@ class TabTrayView(
counter_text.text = updateTabCounter(browserState.normalTabs.size)
updateTabCounterContentDescription(browserState.normalTabs.size)
adjustNewTabButtonForNormalMode()
adjustNewTabButtonsForNormalMode()
}
private fun toggleUIMultiselect(multiselect: Boolean) {
@ -572,7 +596,7 @@ class TabTrayView(
if (multiselect) {
R.dimen.tab_tray_multiselect_handle_height
} else {
R.dimen.tab_tray_normal_handle_height
R.dimen.bottom_sheet_handle_height
}
)
if (useTopTabsTray) {
@ -580,7 +604,7 @@ class TabTrayView(
if (multiselect) {
R.dimen.tab_tray_multiselect_handle_bottom_margin
} else {
R.dimen.tab_tray_normal_handle_bottom_margin
R.dimen.top_sheet_handle_bottom_margin
}
)
} else {
@ -588,7 +612,7 @@ class TabTrayView(
if (multiselect) {
R.dimen.tab_tray_multiselect_handle_top_margin
} else {
R.dimen.tab_tray_normal_handle_top_margin
R.dimen.bottom_sheet_handle_top_margin
}
)
}
@ -737,6 +761,7 @@ class TabTrayView(
}
companion object {
private const val TAB_COUNT_SHOW_CFR = 6
private const val DEFAULT_TAB_ID = 0
private const val PRIVATE_TAB_ID = 1
private const val EXPAND_AT_SIZE = 3

@ -43,9 +43,9 @@ class TrackingProtectionOverlay(
}
private fun shouldShowTrackingProtectionOnboarding(session: Session) =
settings.shouldShowTrackingProtectionOnboarding &&
session.trackerBlockingEnabled &&
session.trackersBlocked.isNotEmpty()
session.trackerBlockingEnabled &&
session.trackersBlocked.isNotEmpty() &&
settings.shouldShowTrackingProtectionCfr
@Suppress("MagicNumber", "InflateParams")
private fun showTrackingProtectionOnboarding() {
@ -57,9 +57,9 @@ class TrackingProtectionOverlay(
if (event.action == MotionEvent.ACTION_DOWN) {
metrics.track(Event.ContextualHintETPOutsideTap)
}
return super.onTouchEvent(event)
}
return super.onTouchEvent(event)
}
}
val layout = LayoutInflater.from(context)
.inflate(R.layout.tracking_protection_onboarding_popup, null)
@ -121,6 +121,7 @@ class TrackingProtectionOverlay(
metrics.track(Event.ContextualHintETPDisplayed)
trackingOnboardingDialog.show()
settings.lastCfrShownTimeInMillis = System.currentTimeMillis()
settings.incrementTrackingProtectionOnboardingCount()
}

@ -62,6 +62,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
private const val CFR_COUNT_CONDITION_FOCUS_NOT_INSTALLED = 3
const val ONE_DAY_MS = 60 * 60 * 24 * 1000L
const val THREE_DAYS_MS = 3 * ONE_DAY_MS
const val ONE_WEEK_MS = 60 * 60 * 24 * 7 * 1000L
const val ONE_MONTH_MS = (60 * 60 * 24 * 365 * 1000L) / 12
@ -115,6 +116,14 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = 0L
)
var lastCfrShownTimeInMillis by longPreference(
appContext.getPreferenceKey(R.string.pref_key_last_cfr_shown_time),
default = 0L
)
val canShowCfr: Boolean
get() = (System.currentTimeMillis() - lastCfrShownTimeInMillis) > THREE_DAYS_MS
var waitToShowPageUntilFirstPaint by featureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_wait_first_paint),
default = false,
@ -191,11 +200,10 @@ class Settings(private val appContext: Context) : PreferencesHolder {
private val isActiveSearcher: Boolean
get() = activeSearchCount.value > 2
fun shouldDisplaySearchWidgetCFR(): Boolean =
isActiveSearcher &&
searchWidgetCFRDismissCount.underMaxCount() &&
!searchWidgetInstalled &&
!searchWidgetCFRManuallyDismissed
fun shouldDisplaySearchWidgetCfr(): Boolean = canShowCfr && isActiveSearcher &&
searchWidgetCFRDismissCount.underMaxCount() &&
!searchWidgetInstalled &&
!searchWidgetCFRManuallyDismissed
private val searchWidgetCFRDisplayCount = counterPreference(
appContext.getPreferenceKey(R.string.pref_key_search_widget_cfr_display_count)
@ -284,8 +292,8 @@ class Settings(private val appContext: Context) : PreferencesHolder {
private var trackingProtectionOnboardingShownThisSession = false
var isOverrideTPPopupsForPerformanceTest = false
val shouldShowTrackingProtectionOnboarding: Boolean
get() = !isOverrideTPPopupsForPerformanceTest &&
val shouldShowTrackingProtectionCfr: Boolean
get() = !isOverrideTPPopupsForPerformanceTest && canShowCfr &&
(trackingProtectionOnboardingCount.underMaxCount() &&
!trackingProtectionOnboardingShownThisSession)
@ -655,8 +663,9 @@ class Settings(private val appContext: Context) : PreferencesHolder {
private val userNeedsToVisitInstallableSites: Boolean
get() = pwaInstallableVisitCount.underMaxCount()
val shouldShowPwaOnboarding: Boolean
val shouldShowPwaCfr: Boolean
get() {
if (!canShowCfr) return false
// We only want to show this on the 3rd time a user visits a site
if (userNeedsToVisitInstallableSites) return false
@ -682,6 +691,14 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = true
)
val shouldShowOpenInAppCfr: Boolean
get() = canShowCfr && shouldShowOpenInAppBanner
var shouldShowAutoCloseTabsBanner by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_should_show_auto_close_tabs_banner),
default = true
)
@VisibleForTesting(otherwise = PRIVATE)
internal val trackingProtectionOnboardingCount = counterPreference(
appContext.getPreferenceKey(R.string.pref_key_tracking_protection_onboarding),
@ -838,8 +855,9 @@ class Settings(private val appContext: Context) : PreferencesHolder {
appContext.getPreferenceKey(R.string.pref_key_private_mode_opened)
)
val showPrivateModeContextualFeatureRecommender: Boolean
val showPrivateModeCfr: Boolean
get() {
if (!canShowCfr) return false
val focusInstalled = MozillaProductDetector
.getInstalledMozillaProducts(appContext as Application)
.contains(MozillaProductDetector.MozillaProducts.FOCUS.productName)

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/mozac_ic_warning"
android:bottom="14dp" />
</layer-list>

@ -13,8 +13,8 @@
<View
android:id="@+id/handle"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginTop="8dp"
android:layout_height="@dimen/bottom_sheet_handle_height"
android:layout_marginTop="@dimen/bottom_sheet_handle_top_margin"
android:background="@color/secondary_text_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

@ -23,6 +23,15 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintWidth_percent="0.1" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/infoBanner"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/foundation_normal_theme"
app:layout_constraintBottom_toTopOf="@id/topBar"/>
<TextView
android:id="@+id/tab_tray_empty_view"
android:layout_width="0dp"
@ -36,7 +45,7 @@
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/divider" />
app:layout_constraintBottom_toTopOf="@id/infoBanner" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/topBar"
@ -53,9 +62,9 @@
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_close_multiselect_content_description"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/multiselect_title"
app:layout_constraintBottom_toBottomOf="@id/multiselect_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/multiselect_title"
app:layout_constraintTop_toTopOf="@id/multiselect_title"
app:srcCompat="@drawable/ic_close"
app:tint="@color/contrast_text_normal_theme" />
@ -70,7 +79,7 @@
app:layout_constraintEnd_toStartOf="@id/collect_multi_select"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/exit_multi_select"
app:layout_constraintStart_toEndOf="@id/exit_multi_select"
app:layout_constraintTop_toTopOf="parent"
tools:text="3 selected" />
@ -104,9 +113,9 @@
android:layout_marginStart="0dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tabs_screen_close_screen_description"
app:layout_constraintBottom_toBottomOf="@+id/tab_layout"
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/tab_layout"
app:layout_constraintTop_toTopOf="@id/tab_layout"
app:srcCompat="@drawable/mozac_ic_back"
app:tint="@color/primary_text_normal_theme" />
@ -176,7 +185,7 @@
android:background="@color/tab_tray_item_divider_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/topBar" />
app:layout_constraintBottom_toTopOf="@id/infoBanner" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tabsTray"
@ -187,6 +196,6 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/divider" />
app:layout_constraintBottom_toTopOf="@id/divider" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -14,8 +14,8 @@
<View
android:id="@+id/handle"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginTop="8dp"
android:layout_height="@dimen/bottom_sheet_handle_height"
android:layout_marginTop="@dimen/bottom_sheet_handle_top_margin"
android:visibility="gone"
android:background="@color/secondary_text_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
@ -23,6 +23,15 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.1" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/infoBanner"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/foundation_normal_theme"
app:layout_constraintTop_toBottomOf="@id/topBar"/>
<TextView
android:id="@+id/tab_tray_empty_view"
android:layout_width="0dp"
@ -37,14 +46,14 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/topBar" />
app:layout_constraintTop_toBottomOf="@id/infoBanner" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/foundation_normal_theme"
app:layout_constraintTop_toBottomOf="@+id/handle">
app:layout_constraintTop_toBottomOf="@id/handle">
<ImageButton
android:id="@+id/exit_multi_select"
@ -54,9 +63,9 @@
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_close_multiselect_content_description"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/multiselect_title"
app:layout_constraintBottom_toBottomOf="@id/multiselect_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/multiselect_title"
app:layout_constraintTop_toTopOf="@id/multiselect_title"
app:srcCompat="@drawable/ic_close"
app:tint="@color/contrast_text_normal_theme" />
@ -71,7 +80,7 @@
app:layout_constraintEnd_toStartOf="@id/collect_multi_select"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/exit_multi_select"
app:layout_constraintStart_toEndOf="@id/exit_multi_select"
app:layout_constraintTop_toTopOf="parent"
tools:text="3 selected" />
@ -105,9 +114,9 @@
android:layout_marginStart="0dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tabs_screen_close_screen_description"
app:layout_constraintBottom_toBottomOf="@+id/tab_layout"
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/tab_layout"
app:layout_constraintTop_toTopOf="@id/tab_layout"
app:srcCompat="@drawable/mozac_ic_back"
app:tint="@color/primary_text_normal_theme" />
@ -176,7 +185,7 @@
android:background="@color/tab_tray_item_divider_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/topBar" />
app:layout_constraintTop_toBottomOf="@id/infoBanner" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tabsTray"
@ -188,6 +197,6 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider" />
app:layout_constraintTop_toBottomOf="@id/divider" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -15,14 +15,23 @@
<View
android:id="@+id/handle"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginTop="8dp"
android:layout_height="@dimen/bottom_sheet_handle_height"
android:layout_marginTop="@dimen/bottom_sheet_handle_top_margin"
android:background="@color/secondary_text_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.1" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/infoBanner"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/foundation_normal_theme"
app:layout_constraintTop_toBottomOf="@id/topBar"/>
<TextView
android:id="@+id/tab_tray_empty_view"
android:layout_width="0dp"
@ -37,14 +46,14 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/topBar" />
app:layout_constraintTop_toBottomOf="@id/infoBanner" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="@color/foundation_normal_theme"
app:layout_constraintTop_toBottomOf="@+id/handle">
app:layout_constraintTop_toBottomOf="@id/handle">
<ImageButton
android:id="@+id/exit_multi_select"
@ -54,9 +63,9 @@
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_close_multiselect_content_description"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/multiselect_title"
app:layout_constraintBottom_toBottomOf="@id/multiselect_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/multiselect_title"
app:layout_constraintTop_toTopOf="@id/multiselect_title"
app:srcCompat="@drawable/ic_close"
app:tint="@color/contrast_text_normal_theme" />
@ -71,7 +80,7 @@
app:layout_constraintEnd_toStartOf="@id/collect_multi_select"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/exit_multi_select"
app:layout_constraintStart_toEndOf="@id/exit_multi_select"
app:layout_constraintTop_toTopOf="parent"
tools:text="3 selected" />
@ -163,7 +172,7 @@
android:background="@color/tab_tray_item_divider_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/topBar" />
app:layout_constraintTop_toBottomOf="@id/infoBanner" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tabsTray"
@ -175,6 +184,6 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider" />
app:layout_constraintTop_toBottomOf="@id/divider" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -23,6 +23,15 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintWidth_percent="0.1" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/infoBanner"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/foundation_normal_theme"
app:layout_constraintBottom_toTopOf="@id/topBar"/>
<TextView
android:id="@+id/tab_tray_empty_view"
android:layout_width="0dp"
@ -36,7 +45,7 @@
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/divider" />
app:layout_constraintBottom_toTopOf="@id/infoBanner" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/topBar"
@ -53,9 +62,9 @@
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_close_multiselect_content_description"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/multiselect_title"
app:layout_constraintBottom_toBottomOf="@id/multiselect_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/multiselect_title"
app:layout_constraintTop_toTopOf="@id/multiselect_title"
app:srcCompat="@drawable/ic_close"
app:tint="@color/contrast_text_normal_theme" />
@ -70,7 +79,7 @@
app:layout_constraintEnd_toStartOf="@id/collect_multi_select"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/exit_multi_select"
app:layout_constraintStart_toEndOf="@id/exit_multi_select"
app:layout_constraintTop_toTopOf="parent"
tools:text="3 selected" />
@ -163,7 +172,7 @@
android:background="@color/tab_tray_item_divider_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/topBar" />
app:layout_constraintBottom_toTopOf="@id/infoBanner" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tabsTray"
@ -174,6 +183,6 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/divider" />
app:layout_constraintBottom_toTopOf="@id/divider" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -23,7 +23,7 @@
android:text="@string/sign_in_with_camera"
android:textAppearance="@style/Header16TextStyle"
android:textColor="?primaryText"
android:textSize="18sp"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -58,7 +58,7 @@
android:id="@+id/signInScanButton"
style="@style/PositiveButton"
android:text="@string/sign_in_ready_for_scan"
android:layout_marginVertical="16dp"
android:layout_marginVertical="24dp"
app:icon="@drawable/ic_qr"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -68,9 +68,9 @@
android:id="@+id/signInEmailButton"
style="@style/NeutralButton"
android:text="@string/sign_in_with_email"
android:layout_margin="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginVertical="12dp"
app:layout_constraintTop_toBottomOf="@id/signInScanButton"
app:layout_constraintBottom_toTopOf="@id/createAccount"/>
@ -83,6 +83,7 @@
app:layout_constraintTop_toBottomOf="@id/signInEmailButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginVertical="24dp"
tools:text="@string/sign_in_create_account_text"/>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -23,10 +23,6 @@
app:popUpTo="@id/homeFragment"
app:popUpToInclusive="false" />
<action
android:id="@+id/action_global_search"
app:destination="@id/searchFragment" />
<action
android:id="@+id/action_global_search_dialog"
app:destination="@id/searchDialogFragment" />
@ -136,16 +132,11 @@
android:name="focusOnAddressBar"
android:defaultValue="false"
app:argType="boolean" />
<argument
android:name="session_to_delete"
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
</fragment>
<dialog
android:id="@+id/searchDialogFragment"
android:name="org.mozilla.fenix.searchdialog.SearchDialogFragment"
android:name="org.mozilla.fenix.search.SearchDialogFragment"
tools:layout="@layout/fragment_search_dialog">
<argument
android:name="session_id"
@ -162,25 +153,6 @@
app:argType="org.mozilla.fenix.components.metrics.Event$PerformedSearch$SearchAccessPoint" />
</dialog>
<fragment
android:id="@+id/searchFragment"
android:name="org.mozilla.fenix.search.SearchFragment"
tools:layout="@layout/fragment_search">
<argument
android:name="session_id"
app:argType="string"
app:nullable="true" />
<argument
android:name="pastedText"
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
<argument
android:name="search_access_point"
android:defaultValue="NONE"
app:argType="org.mozilla.fenix.components.metrics.Event$PerformedSearch$SearchAccessPoint" />
</fragment>
<fragment
android:id="@+id/recentlyClosedFragment"
android:name="org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragment"
@ -200,11 +172,6 @@
android:name="org.mozilla.fenix.browser.BrowserFragment"
app:exitAnim="@anim/fade_out"
tools:layout="@layout/fragment_browser">
<action
android:id="@+id/action_browserFragment_to_searchFragment"
app:destination="@id/searchFragment"
app:enterAnim="@anim/fade_in_up"
app:popExitAnim="@anim/fade_out_down" />
<argument
android:name="activeSessionId"
app:argType="string"

@ -47,6 +47,9 @@
<!-- Content description announcement when entering multiselect mode in tab tray -->
<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>
<!-- About content. The first parameter is the name of the application. (For example: Fenix) -->
<string name="about_content">%1$s من Mozilla.</string>
@ -69,6 +72,11 @@
<!-- Text for the negative button -->
<string name="cfr_neg_button_text">لا، شكرًا</string>
<!-- Search widget "contextual feature recommendation" (CFR) -->
<!-- Text for the main message. 'Firefox' intentionally hardcoded here.-->
<string name="search_widget_cfr_message">افتح Firefox بسرعة أكبر. أضف أداة Firefox إلى الشاشة الرئيسية.</string>
<!-- Text for the positive button -->
<string name="search_widget_cfr_pos_button_text">أضف الأداة</string>
<!-- Text for the negative button -->
<string name="search_widget_cfr_neg_button_text">ليس الآن</string>
@ -138,6 +146,8 @@
<string name="browser_menu_install_on_homescreen">ثبّت</string>
<!-- Menu option on the toolbar that takes you to synced tabs page-->
<string name="synced_tabs">الألسنة المُزامنة</string>
<!-- Content description (not visible, for screen readers etc.) for the Resync tabs button -->
<string name="resync_button_content_description">أعِد المزامنة</string>
<!-- Browser menu button that opens the find in page menu -->
<string name="browser_menu_find_in_page">ابحث في الصفحة</string>
<!-- Browser menu button that creates a private tab -->
@ -155,10 +165,10 @@
<string name="browser_menu_open_in_fenix">افتح في %1$s</string>
<!-- Browser menu text shown in custom tabs to indicate this is a Fenix tab
The first parameter is the name of the app defined in app_name (for example: Fenix) -->
<string name="browser_menu_powered_by">تدعمها %1$s</string>
<string name="browser_menu_powered_by">تدعمه %1$s</string>
<!-- Browser menu text shown in custom tabs to indicate this is a Fenix tab
The first parameter is the name of the app defined in app_name (for example: Fenix) -->
<string name="browser_menu_powered_by2">تدعمها %1$s</string>
<string name="browser_menu_powered_by2">تدعمه %1$s</string>
<!-- Browser menu button to put the current page in reader mode -->
<string name="browser_menu_read">منظور القارئ</string>
<!-- Browser menu button content description to close reader mode and return the user to the regular browser -->
@ -261,6 +271,8 @@
<string name="preferences_open_links_in_a_private_tab">افتح الروابط في ألسنة خاصة</string>
<!-- Preference for allowing screenshots to be taken while in a private tab-->
<string name="preferences_allow_screenshots_in_private_mode">اسمح بلقطات الشاشة في التصفّح الخاص</string>
<!-- Will inform the user of the risk of activating Allow screenshots in private browsing option -->
<string name="preferences_screenshots_in_private_mode_disclaimer">إن سمحت فستظهر الألسنة الخاصة حين فتح أكثر من تطبيق</string>
<!-- Preference for adding private browsing shortcut -->
<string name="preferences_add_private_browsing_shortcut">أضِف اختصارًا للتصفح الخاص</string>
<!-- Preference for accessibility -->
@ -317,6 +329,8 @@
<string name="preferences_search_browsing_history">البحث في تأريخ التصفح</string>
<!-- Preference title for switch preference to suggest bookmarks when searching -->
<string name="preferences_search_bookmarks">البحث في العلامات</string>
<!-- Preference title for switch preference to suggest synced tabs when searching -->
<string name="preferences_search_synced_tabs">ابحث في الألسنة المُزامنة</string>
<!-- Preference for account settings -->
<string name="preferences_account_settings">إعدادات الحساب</string>
<!-- Preference for open links in third party apps -->
@ -360,6 +374,11 @@
<!-- Label summary showing never synced -->
<string name="sync_never_synced_summary">تاريخ آخر مزامنة: أبدًا</string>
<!-- Text for displaying the default device name.
The first parameter is the application name, the second is the device manufacturer name
and the third is the device model. -->
<string name="default_device_name_2">%1$s على %2$s %3$s</string>
<!-- Send Tab -->
<!-- Name of the "receive tabs" notification channel. Displayed in the "App notifications" system settings for the app -->
<string name="fxa_received_tab_channel_name">الألسنة المستلمة</string>
@ -450,6 +469,11 @@
<!-- Preference for using the dynamic toolbar -->
<string name="preference_gestures_dynamic_toolbar">مرّر لإخفاء شريط الأدوات</string>
<!-- Preference for switching tabs by swiping horizontally on the toolbar -->
<string name="preference_gestures_swipe_toolbar_switch_tabs">مرّر شريط الأدوات إلى اليمين واليسار للتبديل بين الألسنة</string>
<!-- Preference for showing the opened tabs by swiping up on the toolbar-->
<string name="preference_gestures_swipe_toolbar_show_tabs">مرّر شريط الأدوات إلى أعلى لفتح الألسنة</string>
<!-- Library -->
<!-- Option in Library to open Sessions page -->
<string name="library_sessions">الجلسات</string>
@ -690,6 +714,10 @@
<string name="bookmark_add_folder_fragment_label">أضِف مجلدًا</string>
<!-- Bookmark select folder screen title -->
<string name="bookmark_select_folder_fragment_label">اختر مجلدًا</string>
<!-- Bookmark editing error missing title -->
<string name="bookmark_empty_title_error">يجب أن يكون للعلامة عنوان</string>
<!-- Bookmark editing error missing or improper URL -->
<string name="bookmark_invalid_url_error">مسار غير صحيح</string>
<!-- Bookmark screen message for empty bookmarks folder -->
<string name="bookmarks_empty_message">ما من علامات هنا</string>
<!-- Bookmark snackbar message on deletion
@ -824,6 +852,8 @@
<string name="sync_connect_device_dialog">لِج إلى Firefox على جهاز واحد آخر على الأقل لإرسال لسان إليه.</string>
<!-- Confirmation dialog button -->
<string name="sync_confirmation_button">فهمت</string>
<!-- Share error message -->
<string name="share_error_snackbar">تعذرت المشاركة مع هذا التطبيق</string>
<!-- Add new device screen title -->
<string name="sync_add_new_device_title">أرسِل إلى جهاز</string>
<!-- Text for the warning message on the Add new device screen -->
@ -839,8 +869,12 @@
<string name="notification_pbm_channel_name">جلسة تصفح خاصة</string>
<!-- Text shown in the notification that pops up to remind the user that a private browsing session is active. -->
<string name="notification_pbm_delete_text">حذف الألسنة الخاصة</string>
<!-- Text shown in the notification that pops up to remind the user that a private browsing session is active. -->
<string name="notification_pbm_delete_text_2">أغلِق الألسنة الخاصة</string>
<!-- Notification action to open Fenix and resume the current browsing session. -->
<string name="notification_pbm_action_open">افتح</string>
<!-- 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>
<!-- Text shown in snackbar when user renames a collection -->
@ -944,9 +978,25 @@
<!-- Tips -->
<!-- text for firefox preview moving tip header "Firefox Preview" and "Firefox Nightly" are intentionally hardcoded -->
<string name="tip_firefox_preview_moved_header">صار Firefox Preview الآن Firefox Nightly</string>
<!-- text for firefox preview moving tip description -->
<string name="tip_firefox_preview_moved_description">تُحدّث النسخة الليلية من Firefox في كل ليلة وتُضاف إليها مزايا تجريبية جديدة.
ولكن قد يكون استقرارها أقل من العادية. نزّل متصفّح بيتا لتجربة أكثر استقرارًا.</string>
<!-- text for firefox preview moving tip button. "Firefox for Android Beta" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_button_2">نزّل نسخة Firefox لأندرويد التجريبية</string>
<!-- text for firefox preview moving tip header. "Firefox Nightly" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_header_preview_installed">انتقلت النسخة الليلية من Firefox من هنا</string>
<!-- text for firefox preview moving tip description -->
<string name="tip_firefox_preview_moved_description_preview_installed">لن يتلقى هذا التطبيق تحديثات الأمان بعد الآن. يُنصح بالتوقف عن استخدام هذا التطبيق والانتقال إلى النسخة الليلية الجديدة.
لنقل علاماتك وجلسات الولوج والتأريخ إلى تطبيق آخر، افتح حساب Firefox.</string>
<!-- text for firefox preview moving tip button -->
<string name="tip_firefox_preview_moved_button_preview_installed">انتقل إلى النسخة الليلية الجديدة</string>
<!-- text for firefox preview moving tip header. "Firefox Nightly" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_header_preview_not_installed">انتقلت النسخة الليلية من Firefox من هنا</string>
<!-- text for firefox preview moving tip description -->
<string name="tip_firefox_preview_moved_description_preview_not_installed">لن يتلقى هذا التطبيق تحديثات الأمان بعد الآن. ننصح بتنزيل النسخة الليلية الجديدة والتوقف عن استخدام هذا التطبيق.
لنقل علاماتك وجلسات الولوج والتأريخ إلى تطبيق آخر، افتح حساب Firefox.</string>
<!-- text for firefox preview moving tip button -->
<string name="tip_firefox_preview_moved_button_preview_not_installed">نزّل النسخة الليلية الجديدة</string>
@ -961,6 +1011,21 @@
<string name="onboarding_feature_section_header">تعرف على %s</string>
<!-- text for the "What's New" onboarding card header -->
<string name="onboarding_whats_new_header1">اعرف ما الجديد</string>
<!-- text for the "what's new" onboarding card description
The first parameter is the short name of the app (e.g. Firefox) -->
<string name="onboarding_whats_new_description">ألديك أسئلة عن متصفّح %s الذي أعدنا تصميمه؟ أتريد معرفة ما تغيّر؟</string>
<!-- text for underlined clickable link that is part of "what's new" onboarding card description that links to an FAQ -->
<string name="onboarding_whats_new_description_linktext">ستجد هنا إجابات أسئلتك</string>
<!-- text for the Firefox account onboarding sign in card header -->
<string name="onboarding_account_sign_in_header">اشرع الآن بمزامنة العلامات وكلمات السر وغيرها الكثير عبر حساب Firefox.</string>
<!-- Text for the button to learn more about signing in to your Firefox account -->
<string name="onboarding_manual_sign_in_learn_more">اطّلع على المزيد</string>
<!-- text for the firefox account onboarding card header when we detect you're already signed in to
another Firefox browser. (The word `Firefox` should not be translated)
The first parameter is the email of the detected user's account -->
<string name="onboarding_firefox_account_auto_signin_header_3">ولجت ببريد %s على متصفّح Firefox آخر على هذا الجهاز. أتريد الولوج بنفس الحساب؟</string>
<!-- text for the button to confirm automatic sign-in -->
<string name="onboarding_firefox_account_auto_signin_confirm">نعم، سألج بنفس الحساب</string>
<!-- text for the automatic sign-in button while signing in is in process -->
<string name="onboarding_firefox_account_signing_in">تَلج الآن…</string>
<!-- text for the button to manually sign into Firefox account. The word "Firefox" should not be translated -->
@ -975,6 +1040,10 @@
<string name="onboarding_tracking_protection_strict_button">صارم (مستحسن)</string>
<!-- text for tracking protection radio button option for strict level of blocking -->
<string name="onboarding_tracking_protection_strict_option">صارم</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">اختر جهةً</string>
<!-- text for the private browsing onboarding card header -->
<string name="onboarding_private_browsing_header">تصفّح بخصوصية</string>
<!-- text for the private browsing onbording card button, that launches settings -->
@ -1312,7 +1381,7 @@
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">حسنًا، فهمت</string>
<!-- depcrecated: text for the firefox account onboarding card header
<!-- Deprecated: text for the firefox account onboarding card header
The first parameter is the name of the app (e.g. Firefox Preview) -->
<string name="onboarding_firefox_account_header">استغلّ %s إلى أقصى حد.</string>

@ -282,6 +282,8 @@
<string name="preferences_theme">Тэма</string>
<!-- Preference for customizing the home screen -->
<string name="preferences_home">Хатняя старонка</string>
<!-- Preference for gestures based actions -->
<string name="preferences_gestures">Жэсты</string>
<!-- Preference for settings related to visual options -->
<string name="preferences_customize">Уладкаванне</string>
@ -317,8 +319,12 @@
<string name="preferences_search_browsing_history">Пошук у гісторыі аглядання</string>
<!-- Preference title for switch preference to suggest bookmarks when searching -->
<string name="preferences_search_bookmarks">Пошук у закладках</string>
<!-- Preference title for switch preference to suggest synced tabs when searching -->
<string name="preferences_search_synced_tabs">Шукаць у сінхранізаваных картках</string>
<!-- Preference for account settings -->
<string name="preferences_account_settings">Налады ўліковага запісу</string>
<!-- Preference for enabling url autocomplete-->
<string name="preferences_enable_autocomplete_urls">Аўтазапаўненне URL-адрасоў</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 -->
@ -450,6 +456,10 @@
<!-- Preference for using following device theme -->
<string name="preference_follow_device_theme">Тэма прылады</string>
<!-- Gestures Preferences-->
<!-- Preferences for using pull to refresh in a webpage -->
<string name="preference_gestures_website_pull_to_refresh">Пацягніце, каб абнавіць</string>
<!-- Library -->
<!-- Option in Library to open Sessions page -->
<string name="library_sessions">Сеансы</string>

@ -1162,6 +1162,8 @@ Fodd bynnag, gall fod yn llai sefydlog. Llwythwch ein porwr Beta i gael profiad
<string name="sign_in_with_camera">Mewngofnodi gydach camera</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Defnyddiwch e-bost yn lle hynny</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[Dim cyfrif? <u>Crëwch un</u> i gydweddu Firefox rhwng dyfeisiau.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Bydd Firefox yn peidio cydweddu eich cyfrif ond ni fydd yn dileu eich data pori ar y ddyfais hon.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1270,6 +1272,11 @@ Fodd bynnag, gall fod yn llai sefydlog. Llwythwch ein porwr Beta i gael profiad
The first parameter is the app name -->
<string name="open_source_licenses_title">%s| Llyfrgelloedd OSS</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Ailgyfeirio Tracwyr</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Yn clirio cwcis wediu gosod i ailgyfeirio i wefannau tracio hysbys.</string>
<!-- About page link text to open support link -->
<string name="about_support">Cefnogaeth</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -1197,6 +1197,8 @@
<string name="sign_in_with_camera">Melden Sie sich mit Ihrer Kamera an</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Stattdessen E-Mail-Adresse verwenden</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[Kein Konto? <u>Erstellen Sie eines</u>, um Firefox zwischen Geräten zu synchronisieren.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox beendet die Synchronisation mit Ihrem Konto, löscht aber keine Surf-Daten auf diesem Gerät.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1304,6 +1306,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | OSS-Bibliotheken</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Weiterleitungs-Tracker</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Löscht Cookies, die durch Weiterleitungen zu bekannten Tracking-Websites gesetzt wurden.</string>
<!-- About page link text to open support link -->
<string name="about_support">Hilfe</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -1164,6 +1164,8 @@
<string name="sign_in_with_camera">Pśizjawśo se ze swójeju kameru</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">E-mail město togo wužywaś</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[Žedno konto? <u>Załožćo take</u>, aby Firefox mjazy rědami synchronizěrował.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox pśestanjo z wašym kontom synchronizěrowaś, ale njewulašujo pśeglědowańske daty na toś tom rěźe.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1274,6 +1276,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | OSS-biblioteki</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Dalejpósrědnjańske pśeslědowaki</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Wulašujo cookije, kótarež su se stajili pśez dalejpósrědnjenja k znatym slědujucym websedłam.</string>
<!-- About page link text to open support link -->
<string name="about_support">Pomoc</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -1155,6 +1155,8 @@
<string name="sign_in_with_camera">Sign in with your camera</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Use email instead</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[No account? <u>Create one</u> to sync Firefox between devices.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox will stop syncing with your account, but wont delete any of your browsing data on this device.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1262,6 +1264,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | OSS Libraries</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Redirect Trackers</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Clears cookies set by redirects to known tracking websites.</string>
<!-- About page link text to open support link -->
<string name="about_support">Support</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -1182,6 +1182,8 @@
<string name="sign_in_with_camera">Inicia sesión con tu cámara</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Use el correo electrónico en su lugar</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[¿No tenés cuenta? <u> Creá una </u> para sincronizar Firefox entre dispositivos.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox va a dejar de sincronizar con tu cuenta pero no va a eliminar ningún dato de navegación en este dispositivo.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1290,6 +1292,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | Bibliotecas OSS</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Redirigir rastreadores</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Borra las cookies establecidas por redireccionamientos a sitios web de rastreo conocidos.</string>
<!-- About page link text to open support link -->
<string name="about_support">Ayuda</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -144,6 +144,8 @@
<string name="browser_menu_install_on_homescreen">Instalar</string>
<!-- Menu option on the toolbar that takes you to synced tabs page-->
<string name="synced_tabs">Pestañas sincronizadas</string>
<!-- Content description (not visible, for screen readers etc.) for the Resync tabs button -->
<string name="resync_button_content_description">Resincronizar</string>
<!-- Browser menu button that opens the find in page menu -->
<string name="browser_menu_find_in_page">Buscar en la página</string>
<!-- Browser menu button that creates a private tab -->
@ -1159,6 +1161,8 @@
<string name="sign_in_with_camera">Conéctate con tu cámara</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">O usa tu correo</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[¿No tienes cuenta? <u>Crea una</u> para sincronizar Firefox entre dispositivos.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox dejará de sincronizarse con tu cuenta, pero no se borrarán los datos de navegación del dispositivo.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1266,6 +1270,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | Bibliotecas OSS</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Rastreadores de redirección</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Limpia las cookies creadas por redirecciones a sitios web de seguimiento conocidos.</string>
<!-- About page link text to open support link -->
<string name="about_support">Ayuda</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -1178,6 +1178,8 @@
<string name="sign_in_with_camera">Kirjaudu sisään kamerallasi</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Käytä sähköpostia</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[Vailla tiliä? <u>Luo tili</u> synkronoidaksesi Firefox laitteiden välillä.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox lopettaa tilisi synkronoinnin, mutta ei poista mitään selaustietoja tältä laitteelta.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1286,6 +1288,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | Avoimen lähdekoodin kirjastot</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Uudelleenohjausseuraimet</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Tyhjentää evästeet, jotka on asetettu tunnetuille seurantasivustoille johtavilla uudelleenohjauksilla.</string>
<!-- About page link text to open support link -->
<string name="about_support">Tuki</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -1180,6 +1180,8 @@
<string name="sign_in_with_camera">Eñepyrũ tembiapo ne raãngamýi ndive</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Eipuru ñandutiveve</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[¿Ndererekói mbaete? <u>Emoheñói</u> embojuehe hag̃ua Firefox mbaeoka ndive.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox nombojuehemoãvéima ne mbaete, hákatu nomboguemoãi ne kundahára mbaekuaarã ko mbaeoka pegua.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1292,6 +1294,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | OSS Arandukarenda</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Embohapejey tapykuehoha</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Embogue umi kookie oñembohapejeýva ñanduti renda rapykuehohápe.</string>
<!-- About page link text to open support link -->
<string name="about_support">Ñepytyvõ</string>

@ -1166,6 +1166,8 @@
<string name="sign_in_with_camera">Přizjewće so ze swojej kameru</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">E-mejl město toho wužiwać</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[Žane konto? <u>Załožće tajke</u>, zo byšće Firefox mjez gratami synchronizował.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox přestanje z wašim kontom synchronizować, ale njezhaša přehladowanske daty na tutym graće.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1273,6 +1275,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | OSS-biblioteki</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Sposrědkowanske přesćěhowaki</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Zhaša placki, kotrež su so přez dalesposrědkowanja k znatym slědowacym websydłam stajili.</string>
<!-- About page link text to open support link -->
<string name="about_support">Pomoc</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -1174,6 +1174,8 @@
<string name="sign_in_with_camera">Jelentkezzen be a kamerájával</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">E-mail használata ehelyett</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[Nincs fiókja? <u>Hozzon létre egyet</u>, hogy szinkronizálja a Firefoxot az eszközök között.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">A Firefox leállítja a szinkronizációt a fiókjával, de nem töröl semmilyen böngészési adatot erről az eszközről.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1283,6 +1285,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | Nyílt forráskódú programkönyvtárak</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Nyomkövetők átirányítása</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Törli az ismert nyomkövető webhelyekre történő átirányítással beállított sütiket.</string>
<!-- About page link text to open support link -->
<string name="about_support">Támogatás</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -1157,6 +1157,8 @@
<string name="sign_in_with_camera">התחברות באמצעות המצלמה שלך</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">שימוש בדוא״ל במקום</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[אין לך חשבון? <u>באפשרותך ליצור אחד</u> כדי לסנכרן את Firefox בין מכשירים.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox יפסיק להסתנכרן עם החשבון שלך, אבל לא ימחק את נתוני הגלישה שלך ממכשיר זה.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1230,7 +1232,7 @@
<!-- Category of trackers (cross-site tracking cookies) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_cookies_title">עוגיות מעקב חוצות אתרים</string>
<!-- Description of cross-site tracking cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_cookies_description">חסימת עוגיות שרשתות פרסומות וחברות ניתוח תעבורה משתמשות בהן כדי לאסוף פרופיל על נתוני הגלישה שלך על פני מגוון רחב של אתרים.</string>
<string name="etp_cookies_description">חוסם עוגיות שרשתות פרסומות וחברות ניתוח תעבורה משתמשות בהן כדי לאסוף פרופיל על נתוני הגלישה שלך על פני מגוון רחב של אתרים.</string>
<!-- Category of trackers (cryptominers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_cryptominers_title">כורי מטבעות דיגיטליים</string>
<!-- Description of cryptominers that can be blocked by Enhanced Tracking Protection -->
@ -1265,6 +1267,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | ספריות OSS</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">רכיבי מעקב של הפניות</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">מנקה עוגיות המוגדרות על־ידי הפניות לאתרי מעקב ידועים.</string>
<!-- About page link text to open support link -->
<string name="about_support">תמיכה</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->
@ -1279,6 +1286,9 @@
<!-- 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 -->

@ -48,7 +48,7 @@
<string name="tab_tray_multiselect_selected_content_description">მონიშნულია</string>
<!-- About content. The first parameter is the name of the application. (For example: Fenix) -->
<string name="about_content">%1$s შექმნა @fork-maintainers-მ.</string>
<string name="about_content">%1$s შემქმნელი @fork-maintainers.</string>
<!-- Private Browsing -->
<!-- Title for private session option -->
@ -143,6 +143,8 @@
<string name="browser_menu_install_on_homescreen">დაყენება</string>
<!-- Menu option on the toolbar that takes you to synced tabs page-->
<string name="synced_tabs">დასინქრონებული ჩანართები</string>
<!-- Content description (not visible, for screen readers etc.) for the Resync tabs button -->
<string name="resync_button_content_description">კვლავ დასინქრონება</string>
<!-- Browser menu button that opens the find in page menu -->
<string name="browser_menu_find_in_page">პოვნა გვერდზე</string>
<!-- Browser menu button that creates a private tab -->
@ -325,6 +327,8 @@
<string name="preferences_search_browsing_history">ძიება დათვალიერების ისტორიაში</string>
<!-- Preference title for switch preference to suggest bookmarks when searching -->
<string name="preferences_search_bookmarks">სანიშნების ძიება</string>
<!-- Preference title for switch preference to suggest synced tabs when searching -->
<string name="preferences_search_synced_tabs">დასინქრონებული ჩანართების მოძიება</string>
<!-- Preference for account settings -->
<string name="preferences_account_settings">ანგარიშის პარამეტრები</string>
<!-- Preference for enabling url autocomplete-->
@ -1234,7 +1238,7 @@
<!-- Category of trackers (cross-site tracking cookies) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_cookies_title">საიტთაშორისი მეთვალყურე ფუნთუშები</string>
<!-- Description of cross-site tracking cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_cookies_description">ზღუდავს ფუნთუშებს, რომლებსაც სარეკლამო და ანალიტიკური კომპანიები იყენებენ თქვენზე მონაცემების ერთიანად აღრიცხვისთვის ბევრ საიტზე.</string>
<string name="etp_cookies_description">ზღუდავს ფუნთუშებს, რომლებსაც სარეკლამო და ანალიტიკური კომპანიები იყენებენ თქვენზე მონაცემების ერთიანად შეგროვებისთვის, სხვადასხვა საიტიდან.</string>
<!-- Category of trackers (cryptominers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_cryptominers_title">კრიპტოგამომმუშავებლები</string>
<!-- Description of cryptominers that can be blocked by Enhanced Tracking Protection -->
@ -1405,7 +1409,7 @@
<!-- Title of warning dialog if users have no device authentication set up -->
<string name="logins_warning_dialog_title">დაიცავით თქვენი ანგარიშები</string>
<!-- Message of warning dialog if users have no device authentication set up -->
<string name="logins_warning_dialog_message">დააყენეთ მოწყობილობის ჩასაკეტად მოსახაზი, PIN-კოდი ან პაროლი, თქვენი შენახული ანგარიშების მონაცემებთან, უცხო პირების წვდომის აღსაკვეთა.</string>
<string name="logins_warning_dialog_message">დააყენეთ მოწყობილობის ჩასაკეტად მოსახაზი, PIN-კოდი ან პაროლი, თქვენი შენახული ანგარიშების მონაცემებთან, უცხო პირების წვდომის აღსაკვეთა.</string>
<!-- Negative button to ignore warning dialog if users have no device authentication set up -->
<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 -->

@ -1155,6 +1155,8 @@
<string name="sign_in_with_camera">Камерамен кіріңіз</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Оның орнына эл. поштаны пайдалану</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[Тіркелгіңіз жоқ па? Firefox-ты құрылғылар арасында синхрондау үшін, <u>тіркелгі жасаңыз</u>.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox тіркелгіңізбен синхрондауды тоқтатады, бірақ, бұл құрылғыда барлық шолу деректері қалады.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1267,6 +1269,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | Ашық библиотекалар</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Қайта бағыттайтын трекерлер</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Белгілі бақылайтын веб-сайттарға қайта бағыттау арқылы орнатылған cookie файлдарын тазарту.</string>
<!-- About page link text to open support link -->
<string name="about_support">Қолдау</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -1210,6 +1210,8 @@
<string name="sign_in_with_camera">카메라로 로그인</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">대신 이메일 사용</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[계정이 없습니까? 기기 간에 Firefox를 동기화하려면 <u>하나를 만드세요</u>.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox가 계정과의 동기화를 중단하지만 이 기기의 사용자 탐색 데이터는 삭제하지 않습니다.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1319,6 +1321,12 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | OSS 라이브러리</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">트래커 리디렉션</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">알려진 추적 웹 사이트로 리디렉션하여 설정된 쿠키를 지웁니다.</string>
<!-- About page link text to open support link -->
<string name="about_support">지원</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -1178,6 +1178,8 @@
<string name="sign_in_with_camera">Logg inn med kameraet ditt</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Bruk e-post i stedet</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[Ingen konto? <u>Opprett en</u> for å synkronisere Firefox mellom enheter.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox vil stoppe synkroniseringen med kontoen din, men vil ikke slette noen av dine nettleserdata på denne enheten.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1290,6 +1292,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | OSS-bibliotek</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Omdirigeringssporere</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Fjerner infokapsler satt av omdirigeringer til kjente sporingsnettsteder.</string>
<!-- About page link text to open support link -->
<string name="about_support">Brukerstøtte</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -1176,6 +1176,8 @@
<string name="sign_in_with_camera">Logg inn med kameraet ditt</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Bruk e-post i staden</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[Ingen konto? <u>Lag ein</u> for å synkronisere Firefox mellom einingar.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox vil stoppe synkroniseringa med kontoen din, men vil ikkje slette nettleserdataa dine på denne eininga.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->

@ -139,6 +139,8 @@
<string name="browser_menu_install_on_homescreen">Installar</string>
<!-- Menu option on the toolbar that takes you to synced tabs page-->
<string name="synced_tabs">Onglets sincronizats</string>
<!-- Content description (not visible, for screen readers etc.) for the Resync tabs button -->
<string name="resync_button_content_description">Tornar sincronizar</string>
<!-- Browser menu button that opens the find in page menu -->
<string name="browser_menu_find_in_page">Recercar dins la pagina</string>
<!-- Browser menu button that creates a private tab -->

@ -1163,6 +1163,8 @@
<string name="sign_in_with_camera">Entre com sua câmera</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Usar e-mail</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[Não tem conta? <u>Crie uma</u> para sincronizar o Firefox entre dispositivos.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">O Firefox deixará de sincronizar com sua conta, mas não excluirá seus dados de navegação neste dispositivo.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1274,6 +1276,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | Bibliotecas de código aberto</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Rastreadores de redirecionamento</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Limpa cookies definidos por redirecionamento de sites de rastreamento conhecidos.</string>
<!-- About page link text to open support link -->
<string name="about_support">Suporte</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -146,6 +146,8 @@
<string name="browser_menu_install_on_homescreen">Namesti</string>
<!-- Menu option on the toolbar that takes you to synced tabs page-->
<string name="synced_tabs">Sinhronizirani zavihki</string>
<!-- Content description (not visible, for screen readers etc.) for the Resync tabs button -->
<string name="resync_button_content_description">Znova sinhroniziraj</string>
<!-- Browser menu button that opens the find in page menu -->
<string name="browser_menu_find_in_page">Najdi na strani</string>
<!-- Browser menu button that creates a private tab -->
@ -268,6 +270,8 @@
<string name="preferences_open_links_in_a_private_tab">Odpri povezave v zasebnem zavihku</string>
<!-- Preference for allowing screenshots to be taken while in a private tab-->
<string name="preferences_allow_screenshots_in_private_mode">Dovoli zajemanje posnetkov zaslona v zasebnem brskanju</string>
<!-- Will inform the user of the risk of activating Allow screenshots in private browsing option -->
<string name="preferences_screenshots_in_private_mode_disclaimer">Če je dovoljeno, bodo zasebni zavihki vidni tudi, ko je odprtih več aplikacij</string>
<!-- Preference for adding private browsing shortcut -->
<string name="preferences_add_private_browsing_shortcut">Dodaj bližnjico zasebnega brskanja</string>
<!-- Preference for accessibility -->
@ -328,6 +332,8 @@
<!-- Preference for account settings -->
<string name="preferences_account_settings">Nastavitve računa</string>
<!-- Preference for enabling url autocomplete-->
<string name="preferences_enable_autocomplete_urls">Samodokončaj spletne naslove</string>
<!-- 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 -->

@ -1181,6 +1181,8 @@
<string name="sign_in_with_camera">Logga in med din kamera</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Använd e-post istället</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[Ha du inget konto? <u>Skapa ett</u> för att synkronisera Firefox mellan enheter.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox kommer sluta att synka med ditt konto, men kommer inte att radera din surfdata på den här enheten.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1289,6 +1291,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | OSS bibliotek</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Omdirigera spårare</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Rensar kakor som ställts in av omdirigeringar till kända spårningswebbplatser.</string>
<!-- About page link text to open support link -->
<string name="about_support">Support</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

File diff suppressed because it is too large Load Diff

@ -1162,6 +1162,8 @@
<string name="sign_in_with_camera">Kameranızla giriş yapın</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">E-posta ile giriş yap</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[Hesabınız yok mu? Firefoxu cihazlar arasında eşitlemek için <u>hesap açın</u>.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox artık hesabınızla eşitlenmeyecek ama bu cihazdaki gezinti geçmişiniz silinmeyecek.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1270,6 +1272,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | OSS Kitaplıkları</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Yönlendirme takipçileri</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Bilinen takip sitelerine yapılan yönlendirmelere ait çerezleri temizler.</string>
<!-- About page link text to open support link -->
<string name="about_support">Destek</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -1176,6 +1176,8 @@
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Використати е-пошту</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[Не маєте облікового запису? <u>Створіть його</u> для синхронізації Firefox між пристроями.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox припинить синхронізацію з вашим обліковим записом, але не вилучить жодних даних перегляду на цьому пристрої.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1284,6 +1286,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | Вільні бібліотеки</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Елементи стеження переспрямуванням</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Очищує куки, встановлені переспрямовувачами на відомі вебсайти для стеження.</string>
<!-- About page link text to open support link -->
<string name="about_support">Підтримка</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -1158,6 +1158,8 @@
<string name="sign_in_with_camera">Đăng nhập bằng máy ảnh của bạn</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Sử dụng email thay thế</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[Không có tài khoản? <u>Tạo một cái</u> để đồng bộ hóa Firefox giữa các thiết bị.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox sẽ ngừng đồng bộ hóa với tài khoản của bạn, nhưng sẽ không xóa mọi dữ liệu duyệt web của bạn trên thiết bị này.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1265,6 +1267,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | Thư viện OSS</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">Trình theo dõi chuyển hướng</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">Xóa cookie được đặt bởi chuyển hướng đến các trang web theo dõi đã biết.</string>
<!-- About page link text to open support link -->
<string name="about_support">Hỗ trợ</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -1203,6 +1203,8 @@
<string name="sign_in_with_camera">使用相机登录</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">改用电子邮件</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[尚无账户?<u>立即创建</u>以在设备间同步 Firefox。]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox 将停止与您的账号的同步,但不会删除此设备上的任何浏览数据。</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1311,6 +1313,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | 开源软件库</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">重定向跟踪器</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">清除通过重定向设置到已知跟踪网站的 Cookie。</string>
<!-- About page link text to open support link -->
<string name="about_support">用户支持</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->
@ -1594,7 +1601,7 @@
<!-- Text displayed when user has disabled tab syncing in Firefox Sync Account -->
<string name="synced_tabs_enable_tab_syncing">请启用标签页同步。</string>
<!-- Text displayed when user has no tabs that have been synced -->
<string name="synced_tabs_no_tabs">您其他设备上的 Firefox 没有打开任何标签页。</string>
<string name="synced_tabs_no_tabs">您其他设备上的 Firefox 没有打开任何标签页。</string>
<!-- 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 -->

@ -1192,6 +1192,8 @@
<string name="sign_in_with_camera">使用攝影機登入</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">改用電子郵件</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[沒有帳號嗎?<u>註冊一組</u>就能在不同裝置間同步 Firefox。]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox 將會停止與您帳號間的同步,但不會刪除此裝置上的任何瀏覽資料。</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
@ -1299,6 +1301,11 @@
The first parameter is the app name -->
<string name="open_source_licenses_title">%s | 開放原始碼程式庫</string>
<!-- Category of trackers (redirect trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_title">重新導向追蹤器</string>
<!-- Description of redirect tracker cookies that can be blocked by Enhanced Tracking Protection -->
<string name="etp_redirect_trackers_description">清除已知網站在重新導向時所設定的 Cookie。</string>
<!-- About page link text to open support link -->
<string name="about_support">技術支援</string>
<!-- About page link text to list of past crashes (like about:crashes on desktop) -->

@ -70,6 +70,12 @@
<!-- Bottom Sheet Fragment card -->
<dimen name="bottom_sheet_corner_radius">8dp</dimen>
<dimen name="bottom_sheet_top_padding">8dp</dimen>
<dimen name="bottom_sheet_handle_height">3dp</dimen>
<dimen name="bottom_sheet_handle_top_margin">8dp</dimen>
<!-- Top Sheet Fragment card -->
<dimen name="top_sheet_handle_bottom_margin">8dp</dimen>
<!-- Browser Toolbar -->
<dimen name="browser_toolbar_height">56dp</dimen>
@ -174,11 +180,8 @@
<dimen name="tab_tray_thumbnail_height_original">69dp</dimen>
<dimen name="tab_tray_favicon_border_radius">4dp</dimen>
<dimen name="tab_tray_multiselect_handle_height">11dp</dimen>
<dimen name="tab_tray_normal_handle_height">3dp</dimen>
<dimen name="tab_tray_multiselect_handle_top_margin">0dp</dimen>
<dimen name="tab_tray_normal_handle_top_margin">8dp</dimen>
<dimen name="tab_tray_multiselect_handle_bottom_margin">0dp</dimen>
<dimen name="tab_tray_normal_handle_bottom_margin">8dp</dimen>
<dimen name="tab_tray_new_collection_padding_start">24dp</dimen>
<dimen name="tab_tray_new_collection_drawable_padding">28dp</dimen>

@ -60,6 +60,7 @@
<string name="pref_key_install_pwa_visits" translatable="false">pref_key_install_pwa_visits</string>
<string name="pref_key_times_app_opened" translatable="false">pref_key_times_app_opened</string>
<string name="pref_key_last_review_prompt_shown_time" translatable="false">pref_key_last_review_prompt_shown_time</string>
<string name="pref_key_last_cfr_shown_time" translatable="false">pref_key_last_cfr_shown_time</string>
<!-- Data Choices -->
<string name="pref_key_telemetry" translatable="false">pref_key_telemetry</string>
@ -212,6 +213,9 @@
<!-- A value of `true` means the Open In App Banner has not been shown yet -->
<string name="pref_key_should_show_open_in_app_banner" translatable="false">pref_key_should_show_open_in_app_banner</string>
<!-- A value of `true` means the Auto Close Tabs Banner has not been shown yet -->
<string name="pref_key_should_show_auto_close_tabs_banner" translatable="false">pref_key_should_show_auto_close_tabs_banner</string>
<string name="pref_key_migrating_from_fenix_nightly_tip" translatable="false">pref_key_migrating_from_fenix_nightly_tip</string>
<string name="pref_key_migrating_from_firefox_nightly_tip" translatable="false">pref_key_migrating_from_firefox_nightly_tip</string>
<string name="pref_key_migrating_from_fenix_tip" translatable="false">pref_key_migrating_from_fenix_tip</string>

@ -1166,11 +1166,8 @@
<string name="sign_in_with_camera">Sign in with your camera</string>
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Use email instead</string>
<!-- Text shown for settings option for create new account description.
'Firefox' intentionally hardcoded here. %s is to be replaced with the create account link text-->
<string name="sign_in_create_account_text">No account? %s to sync Firefox between devices.</string>
<!--Text shown for settings option for create new account link -->
<string name="sign_in_create_account_link">Create one</string>
<!-- Text shown for settings option for create new account text.'Firefox' intentionally hardcoded here.-->
<string name="sign_in_create_account_text"><![CDATA[No account? <u>Create one</u> to sync Firefox between devices.]]></string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Firefox will stop syncing with your account, but wont delete any of your browsing data on this device.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->

@ -19,12 +19,10 @@
<activity-alias
android:name="${applicationId}.App"
android:targetActivity="org.mozilla.fenix.MigrationDecisionActivity"
android:taskAffinity="${applicationId}.BROWSER"
tools:replace="android:targetActivity" />
<activity
android:name="org.mozilla.fenix.MigrationDecisionActivity"
android:taskAffinity="${applicationId}.BROWSER"
android:exported="false" />
<service android:name="org.mozilla.fenix.MigrationService" />

@ -5,6 +5,7 @@
package org.mozilla.fenix.addons
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.RelaxedMockK
@ -15,6 +16,8 @@ import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.addons.AddonsManagementFragmentDirections.Companion.actionAddonsManagementFragmentToAddonDetailsFragment
import org.mozilla.fenix.addons.AddonsManagementFragmentDirections.Companion.actionAddonsManagementFragmentToInstalledAddonDetails
import org.mozilla.fenix.ext.directionsEq
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@ -37,12 +40,16 @@ class AddonsManagementViewTest {
val addon = mockk<Addon> {
every { isInstalled() } returns true
}
every { navController.currentDestination } returns NavDestination("").apply {
id = R.id.addonsManagementFragment
}
managementView.onAddonItemClicked(addon)
val expected = actionAddonsManagementFragmentToInstalledAddonDetails(addon)
verify {
navController.navigate(
directionsEq(actionAddonsManagementFragmentToInstalledAddonDetails(addon))
)
navController.navigate(directionsEq(expected))
}
}
@ -51,14 +58,55 @@ class AddonsManagementViewTest {
val addon = mockk<Addon> {
every { isInstalled() } returns false
}
every { navController.currentDestination } returns NavDestination("").apply {
id = R.id.addonsManagementFragment
}
managementView.onAddonItemClicked(addon)
val expected = AddonsManagementFragmentDirections.actionAddonsManagementFragmentToAddonDetailsFragment(addon)
val expected = actionAddonsManagementFragmentToAddonDetailsFragment(addon)
verify {
navController.navigate(directionsEq(expected))
}
}
@Test
fun `onAddonItemClicked on not installed addon does not navigate if not currently on addonsManagementFragment`() {
val addon = mockk<Addon> {
every { isInstalled() } returns false
}
every { navController.currentDestination } returns NavDestination("").apply {
id = R.id.settingsFragment
}
managementView.onAddonItemClicked(addon)
val expected = actionAddonsManagementFragmentToAddonDetailsFragment(addon)
verify(exactly = 0) {
navController.navigate(directionsEq(expected))
}
}
@Test
fun `onAddonItemClicked on installed addon does not navigate if not currently on addonsManagementFragment`() {
val addon = mockk<Addon> {
every { isInstalled() } returns true
}
every { navController.currentDestination } returns NavDestination("").apply {
id = R.id.settingsFragment
}
managementView.onAddonItemClicked(addon)
val expected = actionAddonsManagementFragmentToAddonDetailsFragment(addon)
verify(exactly = 0) {
navController.navigate(directionsEq(expected))
}
}
@Test
fun `onInstallAddonButtonClicked shows permission dialog`() {
val addon = mockk<Addon>()

@ -9,7 +9,6 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking
import mozilla.components.lib.crash.CrashReporter
import mozilla.components.service.fxa.manager.FxaAccountManager
@ -35,12 +34,8 @@ class AccountAbnormalitiesTest {
assertEquals("userRequestedLogout before account manager was configured", e.message)
}
try {
accountAbnormalities.onAuthenticated(mockk(), mockk())
fail()
} catch (e: IllegalStateException) {
assertEquals("onAuthenticated before account manager was configured", e.message)
}
// This doesn't throw, see method for details.
accountAbnormalities.onAuthenticated(mockk(), mockk())
try {
accountAbnormalities.onLoggedOut()
@ -58,10 +53,7 @@ class AccountAbnormalitiesTest {
val accountManager: FxaAccountManager = mockk(relaxed = true)
val accountAbnormalities = AccountAbnormalities(testContext, crashReporter, this.coroutineContext)
accountAbnormalities.accountManagerInitializedAsync(
accountManager,
CompletableDeferred(Unit).also { it.complete(Unit) }
).await()
accountAbnormalities.accountManagerStarted(accountManager)
// Logout action must be preceded by auth.
accountAbnormalities.userRequestedLogout()
@ -74,10 +66,7 @@ class AccountAbnormalitiesTest {
val accountManager: FxaAccountManager = mockk(relaxed = true)
val accountAbnormalities = AccountAbnormalities(testContext, crashReporter, this.coroutineContext)
accountAbnormalities.accountManagerInitializedAsync(
accountManager,
CompletableDeferred(Unit).also { it.complete(Unit) }
).await()
accountAbnormalities.accountManagerStarted(accountManager)
accountAbnormalities.onAuthenticated(mockk(), mockk())
// So far, so good. A regular logout request while being authenticated.
@ -95,10 +84,7 @@ class AccountAbnormalitiesTest {
val accountManager: FxaAccountManager = mockk(relaxed = true)
val accountAbnormalities = AccountAbnormalities(testContext, crashReporter, this.coroutineContext)
accountAbnormalities.accountManagerInitializedAsync(
accountManager,
CompletableDeferred(Unit).also { it.complete(Unit) }
).await()
accountAbnormalities.accountManagerStarted(accountManager)
// User didn't request this logout.
accountAbnormalities.onLoggedOut()
@ -111,10 +97,7 @@ class AccountAbnormalitiesTest {
val accountManager: FxaAccountManager = mockk(relaxed = true)
val accountAbnormalities = AccountAbnormalities(testContext, crashReporter, this.coroutineContext)
accountAbnormalities.accountManagerInitializedAsync(
accountManager,
CompletableDeferred(Unit).also { it.complete(Unit) }
).await()
accountAbnormalities.accountManagerStarted(accountManager)
accountAbnormalities.onAuthenticated(mockk(), mockk())
verify { crashReporter wasNot Called }
@ -124,10 +107,7 @@ class AccountAbnormalitiesTest {
val accountAbnormalities2 = AccountAbnormalities(testContext, crashReporter, this.coroutineContext)
// mock accountManager doesn't have an account, but we expect it to have one since we
// were authenticated before our "restart".
accountAbnormalities2.accountManagerInitializedAsync(
accountManager,
CompletableDeferred(Unit).also { it.complete(Unit) }
).await()
accountAbnormalities2.accountManagerStarted(accountManager)
assertCaughtException<AbnormalFxaEvent.MissingExpectedAccountAfterStartup>(crashReporter)
}
@ -138,10 +118,7 @@ class AccountAbnormalitiesTest {
val accountManager: FxaAccountManager = mockk(relaxed = true)
val accountAbnormalities = AccountAbnormalities(testContext, crashReporter, this.coroutineContext)
accountAbnormalities.accountManagerInitializedAsync(
accountManager,
CompletableDeferred(Unit).also { it.complete(Unit) }
).await()
accountAbnormalities.accountManagerStarted(accountManager)
// We saw an auth event, then user requested a logout.
accountAbnormalities.onAuthenticated(mockk(), mockk())

@ -78,10 +78,13 @@ class BackgroundServicesTest {
fun `telemetry account observer tracks shared event`() {
val account = mockk<OAuthAccount>()
registry.notifyObservers { onAuthenticated(account, AuthType.Shared) }
verify { metrics.track(Event.SyncAuthFromShared) }
registry.notifyObservers { onAuthenticated(account, AuthType.MigratedReuse) }
verify { metrics.track(Event.SyncAuthFromSharedReuse) }
verify { settings.fxaSignedIn = true }
confirmVerified(metrics, settings)
registry.notifyObservers { onAuthenticated(account, AuthType.MigratedCopy) }
verify { metrics.track(Event.SyncAuthFromSharedCopy) }
}
@Test

@ -21,7 +21,6 @@ import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.search.SearchUseCases
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.top.sites.TopSitesUseCases
import org.junit.Assert.assertEquals
import org.junit.Before
@ -39,23 +38,39 @@ import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.home.HomeScreenViewModel
@RunWith(FenixRobolectricTestRunner::class)
class DefaultBrowserToolbarControllerTest {
@RelaxedMockK private lateinit var activity: HomeActivity
@MockK(relaxUnitFun = true) private lateinit var navController: NavController
@RelaxedMockK private lateinit var onTabCounterClicked: () -> Unit
@RelaxedMockK private lateinit var onCloseTab: (Session) -> Unit
@RelaxedMockK private lateinit var sessionManager: SessionManager
@MockK(relaxUnitFun = true) private lateinit var engineView: EngineView
@MockK private lateinit var currentSession: Session
@RelaxedMockK private lateinit var metrics: MetricController
@RelaxedMockK private lateinit var searchUseCases: SearchUseCases
@RelaxedMockK private lateinit var sessionUseCases: SessionUseCases
@RelaxedMockK private lateinit var browserAnimator: BrowserAnimator
@RelaxedMockK private lateinit var topSitesUseCase: TopSitesUseCases
@RelaxedMockK private lateinit var readerModeController: ReaderModeController
@RelaxedMockK
private lateinit var activity: HomeActivity
@MockK(relaxUnitFun = true)
private lateinit var navController: NavController
@RelaxedMockK
private lateinit var onTabCounterClicked: () -> Unit
@RelaxedMockK
private lateinit var onCloseTab: (Session) -> Unit
@RelaxedMockK
private lateinit var sessionManager: SessionManager
@MockK(relaxUnitFun = true)
private lateinit var engineView: EngineView
@MockK
private lateinit var currentSession: Session
@RelaxedMockK
private lateinit var metrics: MetricController
@RelaxedMockK
private lateinit var searchUseCases: SearchUseCases
@RelaxedMockK
private lateinit var sessionUseCases: SessionUseCases
@RelaxedMockK
private lateinit var browserAnimator: BrowserAnimator
@RelaxedMockK
private lateinit var topSitesUseCase: TopSitesUseCases
@RelaxedMockK
private lateinit var readerModeController: ReaderModeController
@RelaxedMockK
private lateinit var homeViewModel: HomeScreenViewModel
@Before
fun setUp() {
@ -79,10 +94,10 @@ class DefaultBrowserToolbarControllerTest {
@Test
fun handleBrowserToolbarPaste() {
val pastedText = "Mozilla"
val controller = createController(useNewSearchExperience = false)
val controller = createController()
controller.handleToolbarPaste(pastedText)
val directions = BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
val directions = BrowserFragmentDirections.actionGlobalSearchDialog(
sessionId = "1",
pastedText = pastedText
)
@ -93,7 +108,7 @@ class DefaultBrowserToolbarControllerTest {
@Test
fun handleBrowserToolbarPaste_useNewSearchExperience() {
val pastedText = "Mozilla"
val controller = createController(useNewSearchExperience = true)
val controller = createController()
controller.handleToolbarPaste(pastedText)
val directions = BrowserFragmentDirections.actionGlobalSearchDialog(
@ -154,10 +169,10 @@ class DefaultBrowserToolbarControllerTest {
@Test
fun handleToolbarClick() {
val controller = createController(useNewSearchExperience = false)
val controller = createController()
controller.handleToolbarClick()
val expected = BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
val expected = BrowserFragmentDirections.actionGlobalSearchDialog(
sessionId = "1"
)
@ -167,7 +182,7 @@ class DefaultBrowserToolbarControllerTest {
@Test
fun handleToolbarClick_useNewSearchExperience() {
val controller = createController(useNewSearchExperience = true)
val controller = createController()
controller.handleToolbarClick()
val expected = BrowserFragmentDirections.actionGlobalSearchDialog(
@ -194,23 +209,22 @@ class DefaultBrowserToolbarControllerTest {
val controller = createController()
controller.handleTabCounterItemInteraction(item)
verify { navController.navigate(BrowserFragmentDirections.actionGlobalHome(sessionToDelete = "1")) }
verify {
homeViewModel.sessionToDelete = "1"
navController.navigate(BrowserFragmentDirections.actionGlobalHome())
}
assertEquals(BrowsingMode.Normal, browsingModeManager.mode)
}
@Test
fun handleToolbarCloseTabPress() {
val tabsUseCases: TabsUseCases = mockk(relaxed = true)
val removeTabUseCase: TabsUseCases.RemoveTabUseCase = mockk(relaxed = true)
val item = TabCounterMenu.Item.CloseTab
every { sessionManager.sessions } returns emptyList()
every { activity.components.useCases.tabsUseCases } returns tabsUseCases
every { tabsUseCases.removeTab } returns removeTabUseCase
val controller = createController()
controller.handleTabCounterItemInteraction(item)
verify { removeTabUseCase.invoke(currentSession) }
verify { sessionManager.remove(currentSession, selectParentIfExists = true) }
}
@Test
@ -261,18 +275,16 @@ class DefaultBrowserToolbarControllerTest {
private fun createController(
activity: HomeActivity = this.activity,
customTabSession: Session? = null,
useNewSearchExperience: Boolean = false
customTabSession: Session? = null
) = DefaultBrowserToolbarController(
activity = activity,
navController = navController,
metrics = metrics,
engineView = engineView,
browserAnimator = browserAnimator,
homeViewModel = homeViewModel,
customTabSession = customTabSession,
readerModeController = readerModeController,
sessionManager = sessionManager,
useNewSearchExperience = useNewSearchExperience,
onTabCounterClicked = onTabCounterClicked,
onCloseTab = onCloseTab
)

@ -23,6 +23,7 @@ class DownloadItemKtTest {
assertEquals(R.drawable.ic_file_type_zip, downloadItem.copy(contentType = "application/gzip").getIcon())
assertEquals(R.drawable.ic_file_type_apk, downloadItem.copy(contentType = null, fileName = "Fenix.apk").getIcon())
assertEquals(R.drawable.ic_file_type_zip, downloadItem.copy(contentType = null, fileName = "Fenix.zip").getIcon())
assertEquals(R.drawable.ic_file_type_document, downloadItem.copy(contentType = null, fileName = "Fenix.pdf").getIcon())
assertEquals(R.drawable.ic_file_type_default, downloadItem.copy(contentType = null, fileName = null).getIcon())
}
}

@ -410,7 +410,7 @@ class DefaultSessionControlControllerTest {
verify {
navController.navigate(
match<NavDirections> { it.actionId == R.id.action_global_search },
match<NavDirections> { it.actionId == R.id.action_global_search_dialog },
null
)
}

@ -55,7 +55,7 @@ class StartSearchIntentProcessorTest {
verify { metrics.track(Event.SearchWidgetNewTabPressed) }
verify {
navController.navigate(
NavGraphDirections.actionGlobalSearch(
NavGraphDirections.actionGlobalSearchDialog(
sessionId = null,
searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.WIDGET
),

@ -7,16 +7,16 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding
import android.view.LayoutInflater
import android.view.View
import io.mockk.every
import io.mockk.coEvery
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.unmockkObject
import io.mockk.verify
import io.mockk.unmockkObject
import kotlinx.android.synthetic.main.onboarding_automatic_signin.view.*
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.service.fxa.manager.SignInWithShareableAccountResult
import mozilla.components.service.fxa.manager.MigrationResult
import mozilla.components.service.fxa.sharing.ShareableAccount
import mozilla.components.support.test.robolectric.testContext
import org.junit.After
@ -69,13 +69,30 @@ class OnboardingAutomaticSignInViewHolderTest {
}
@Test
fun `sign in on click`() = runBlocking {
fun `sign in on click - MigrationResult Success`() = runBlocking {
val account = mockk<ShareableAccount> {
every { email } returns "email@example.com"
}
coEvery {
backgroundServices.accountManager.migrateFromAccount(account)
} returns MigrationResult.Success
val holder = OnboardingAutomaticSignInViewHolder(view, scope = this)
holder.bind(account)
holder.onClick(view.fxa_sign_in_button)
assertEquals("Signing in…", view.fxa_sign_in_button.text)
assertFalse(view.fxa_sign_in_button.isEnabled)
}
@Test
fun `sign in on click - MigrationResult WillRetry treated the same as Success`() = runBlocking {
val account = mockk<ShareableAccount> {
every { email } returns "email@example.com"
}
every {
backgroundServices.accountManager.signInWithShareableAccountAsync(account)
} returns CompletableDeferred(SignInWithShareableAccountResult.Success)
coEvery {
backgroundServices.accountManager.migrateFromAccount(account)
} returns MigrationResult.WillRetry
val holder = OnboardingAutomaticSignInViewHolder(view, scope = this)
holder.bind(account)
@ -90,9 +107,9 @@ class OnboardingAutomaticSignInViewHolderTest {
val account = mockk<ShareableAccount> {
every { email } returns "email@example.com"
}
every {
backgroundServices.accountManager.signInWithShareableAccountAsync(account)
} returns CompletableDeferred(SignInWithShareableAccountResult.Failure)
coEvery {
backgroundServices.accountManager.migrateFromAccount(account)
} returns MigrationResult.Failure
val holder = OnboardingAutomaticSignInViewHolder(view, scope = this)
holder.bind(account)

@ -0,0 +1,147 @@
/* 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 mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class UtilsKtTest {
@Test
fun `friendly root titles`() {
val url = BookmarkNode(
BookmarkNodeType.ITEM,
"456",
"folder",
0,
"Mozilla",
"http://mozilla.org",
null
)
assertEquals("Mozilla", friendlyRootTitle(testContext, url))
val folder = BookmarkNode(
BookmarkNodeType.FOLDER,
"456",
"folder",
0,
"Folder",
null,
null
)
assertEquals("Folder", friendlyRootTitle(testContext, folder))
val root = folder.copy(guid = "root________", title = "root")
assertEquals("Bookmarks", friendlyRootTitle(testContext, root, withMobileRoot = true))
assertEquals("Desktop Bookmarks", friendlyRootTitle(testContext, root, withMobileRoot = false))
val mobileRoot = folder.copy(guid = "mobile______", title = "mobile")
assertEquals("Bookmarks", friendlyRootTitle(testContext, mobileRoot, withMobileRoot = true))
assertEquals("mobile", friendlyRootTitle(testContext, mobileRoot, withMobileRoot = false))
val menuRoot = folder.copy(guid = "menu________", title = "menu")
assertEquals("Bookmarks Menu", friendlyRootTitle(testContext, menuRoot, withMobileRoot = true))
assertEquals("Bookmarks Menu", friendlyRootTitle(testContext, menuRoot, withMobileRoot = false))
val toolbarRoot = folder.copy(guid = "toolbar_____", title = "toolbar")
assertEquals("Bookmarks Toolbar", friendlyRootTitle(testContext, toolbarRoot, withMobileRoot = true))
assertEquals("Bookmarks Toolbar", friendlyRootTitle(testContext, toolbarRoot, withMobileRoot = false))
val unfiledRoot = folder.copy(guid = "unfiled_____", title = "unfiled")
assertEquals("Other Bookmarks", friendlyRootTitle(testContext, unfiledRoot, withMobileRoot = true))
assertEquals("Other Bookmarks", friendlyRootTitle(testContext, unfiledRoot, withMobileRoot = false))
val almostRoot = folder.copy(guid = "notRoot________", title = "root")
assertEquals("root", friendlyRootTitle(testContext, almostRoot, withMobileRoot = true))
assertEquals("root", friendlyRootTitle(testContext, almostRoot, withMobileRoot = false))
}
@Test
fun `flatNodeList various cases`() {
val url = BookmarkNode(
BookmarkNodeType.ITEM,
"456",
"folder",
0,
"Mozilla",
"http://mozilla.org",
null
)
val url2 = BookmarkNode(
BookmarkNodeType.ITEM,
"8674",
"folder2",
0,
"Mozilla",
"http://mozilla.org",
null
)
assertEquals(emptyList<BookmarkNodeWithDepth>(), url.flatNodeList(null))
val root = BookmarkNode(
BookmarkNodeType.FOLDER,
"root",
null,
0,
"root",
null,
null
)
assertEquals(listOf(BookmarkNodeWithDepth(0, root, null)), root.flatNodeList(null))
assertEquals(emptyList<BookmarkNodeWithDepth>(), root.flatNodeList("root"))
val folder = BookmarkNode(
BookmarkNodeType.FOLDER,
"folder",
root.guid,
0,
"folder",
null,
listOf(url)
)
val folder3 = BookmarkNode(
BookmarkNodeType.FOLDER,
"folder3",
"folder2",
0,
"folder3",
null,
null
)
val folder2 = BookmarkNode(
BookmarkNodeType.FOLDER,
"folder2",
root.guid,
0,
"folder2",
null,
listOf(folder3, url2)
)
val rootWithChildren = root.copy(children = listOf(folder, folder2))
assertEquals(
listOf(
BookmarkNodeWithDepth(0, rootWithChildren, null),
BookmarkNodeWithDepth(1, folder, "root"),
BookmarkNodeWithDepth(1, folder2, "root"),
BookmarkNodeWithDepth(2, folder3, "folder2")
), rootWithChildren.flatNodeList(null)
)
assertEquals(
listOf(
BookmarkNodeWithDepth(0, rootWithChildren, null),
BookmarkNodeWithDepth(1, folder, "root")
), rootWithChildren.flatNodeList(excludeSubtreeRoot = "folder2")
)
}
}

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

Loading…
Cancel
Save