diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 58b4bf9b0..81aa72d6d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/app/build.gradle b/app/build.gradle index fa6fb01f5..3efd92820 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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] diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt index 28ccdad03..f83061359 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt @@ -72,9 +72,9 @@ class HomeScreenTest { verifyGetToKnowHeader() // See What's new - scrollToElementByText("See what’s new") - verifyWhatsNewHeader() - verifyWhatsNewLink() + // scrollToElementByText("See what’s new") + // verifyWhatsNewHeader() + // verifyWhatsNewLink() // Automatic privacy scrollToElementByText("Automatic privacy") diff --git a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt index 84da2f843..e14c6c048 100644 --- a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt +++ b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt @@ -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), diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index 0d33c7c90..2b0617c7c 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -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 */ diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index d18c56462..b1109f73f 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -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 -> diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementView.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementView.kt index 2a6370ed6..4e9e24cab 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementView.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementView.kt @@ -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) { diff --git a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt index 0c843d5cc..2a0d7ddfe 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -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() diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index 11310a2fe..ebd51b122 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -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(), diff --git a/app/src/main/java/org/mozilla/fenix/browser/InfoBanner.kt b/app/src/main/java/org/mozilla/fenix/browser/InfoBanner.kt index be74646d2..d7f4187cc 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/InfoBanner.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/InfoBanner.kt @@ -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() { diff --git a/app/src/main/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserver.kt b/app/src/main/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserver.kt index 82e6e5aea..63b8c2635 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserver.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserver.kt @@ -42,7 +42,7 @@ class OpenInAppOnboardingObserver( if (!loading && !settings.openLinksInExternalApp && - settings.shouldShowOpenInAppBanner && + settings.shouldShowOpenInAppCfr && appLink(session.url).hasExternalApp() ) { infoBanner = InfoBanner( diff --git a/app/src/main/java/org/mozilla/fenix/cfr/SearchWidgetCFR.kt b/app/src/main/java/org/mozilla/fenix/cfr/SearchWidgetCFR.kt index 07d017ce0..2b37185b0 100644 --- a/app/src/main/java/org/mozilla/fenix/cfr/SearchWidgetCFR.kt +++ b/app/src/main/java/org/mozilla/fenix/cfr/SearchWidgetCFR.kt @@ -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) diff --git a/app/src/main/java/org/mozilla/fenix/components/AccountAbnormalities.kt b/app/src/main/java/org/mozilla/fenix/components/AccountAbnormalities.kt index 9f1a137e8..92fb160bb 100644 --- a/app/src/main/java/org/mozilla/fenix/components/AccountAbnormalities.kt +++ b/app/src/main/java/org/mozilla/fenix/components/AccountAbnormalities.kt @@ -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 - ): Deferred { + 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 diff --git a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt index 44f4131fe..c2e92057e 100644 --- a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt +++ b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt @@ -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 } diff --git a/app/src/main/java/org/mozilla/fenix/components/TabCollectionStorage.kt b/app/src/main/java/org/mozilla/fenix/components/TabCollectionStorage.kt index 214012cba..10523589d 100644 --- a/app/src/main/java/org/mozilla/fenix/components/TabCollectionStorage.kt +++ b/app/src/main/java/org/mozilla/fenix/components/TabCollectionStorage.kt @@ -75,8 +75,8 @@ class TabCollectionStorage( return collectionStorage.getTabCollectionsCount() } - fun getCollections(limit: Int = 20): LiveData> { - return collectionStorage.getCollections(limit).asLiveData() + fun getCollections(): LiveData> { + return collectionStorage.getCollections().asLiveData() } fun getCollectionsPaged(): DataSource.Factory { diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt index 1a3be9a71..c01c4998b 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt @@ -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() diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt index ff71896e1..25a289a55 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt @@ -269,7 +269,7 @@ private val Event.wrapper: EventWrapper<*>? is Event.SyncAuthOtherExternal -> EventWrapper( { SyncAuth.otherExternal.record(it) } ) - is Event.SyncAuthFromShared -> EventWrapper( + is Event.SyncAuthFromSharedReuse, Event.SyncAuthFromSharedCopy -> EventWrapper( { SyncAuth.autoLogin.record(it) } ) is Event.SyncAuthRecovered -> EventWrapper( diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt index acd4dae51..d3dc2b392 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt @@ -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" diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt index 5b70eae28..9b932bab1 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt @@ -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) } } } diff --git a/app/src/main/java/org/mozilla/fenix/ext/DownloadItem.kt b/app/src/main/java/org/mozilla/fenix/ext/DownloadItem.kt index 93fa26aea..df4854561 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/DownloadItem.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/DownloadItem.kt @@ -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 } } diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 932f38a98..6df4a4131 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -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() 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 { - 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 diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeScreenViewModel.kt b/app/src/main/java/org/mozilla/fenix/home/HomeScreenViewModel.kt index ae3f3fcc1..3c0b80149 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeScreenViewModel.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeScreenViewModel.kt @@ -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 /** diff --git a/app/src/main/java/org/mozilla/fenix/home/intent/StartSearchIntentProcessor.kt b/app/src/main/java/org/mozilla/fenix/home/intent/StartSearchIntentProcessor.kt index 6b5be6a51..e6f079c8b 100644 --- a/app/src/main/java/org/mozilla/fenix/home/intent/StartSearchIntentProcessor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/intent/StartSearchIntentProcessor.kt @@ -46,7 +46,7 @@ class StartSearchIntentProcessor( out.removeExtra(HomeActivity.OPEN_TO_SEARCH) val directions = source?.let { - NavGraphDirections.actionGlobalSearch( + NavGraphDirections.actionGlobalSearchDialog( sessionId = null, searchAccessPoint = it ) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt index 3ad615f26..c2a35df94 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt @@ -428,7 +428,7 @@ class DefaultSessionControlController( } override fun handlePaste(clipboardText: String) { - val directions = HomeFragmentDirections.actionGlobalSearch( + val directions = HomeFragmentDirections.actionGlobalSearchDialog( sessionId = null, pastedText = clipboardText ) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt index c9f1a3667..a4482e194 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt @@ -94,7 +94,6 @@ private fun onboardingAdapterItems(onboardingState: OnboardingState): List { + 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'. - } } } diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt index 5d203028f..842d1878c 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/Utils.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/Utils.kt index 354b80bac..44a1c00e9 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/Utils.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/Utils.kt @@ -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 = 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 { + 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() +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderAdapter.kt index 7eb347bd3..938426bed 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderAdapter.kt @@ -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(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 { - 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 } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt index b34f22bb4..f4714615a 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt @@ -344,7 +344,7 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl private suspend fun syncHistory() { val accountManager = requireComponents.backgroundServices.accountManager - accountManager.syncNowAsync(SyncReason.User).await() + accountManager.syncNow(SyncReason.User) viewModel.invalidate() } } diff --git a/app/src/main/java/org/mozilla/fenix/push/PushFxaIntegration.kt b/app/src/main/java/org/mozilla/fenix/push/PushFxaIntegration.kt index 061d06ac6..9412435f0 100644 --- a/app/src/main/java/org/mozilla/fenix/push/PushFxaIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/push/PushFxaIntegration.kt @@ -122,7 +122,7 @@ internal class OneTimeMessageDeliveryObserver( authType: AuthType ) { lazyAccount.value.withConstellation { - processRawEventAsync(String(message)) + MainScope().launch { processRawEvent(String(message)) } } MainScope().launch { diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchController.kt b/app/src/main/java/org/mozilla/fenix/search/SearchController.kt deleted file mode 100644 index 058bf715b..000000000 --- a/app/src/main/java/org/mozilla/fenix/search/SearchController.kt +++ /dev/null @@ -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() - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogController.kt b/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt similarity index 92% rename from app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogController.kt rename to app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt index 476a7d615..742f7fab1 100644 --- a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogController.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt @@ -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, diff --git a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt similarity index 98% rename from app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt rename to app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt index ef15773d5..f290638ba 100644 --- a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt @@ -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 { diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchInteractor.kt b/app/src/main/java/org/mozilla/fenix/search/SearchDialogInteractor.kt similarity index 95% rename from app/src/main/java/org/mozilla/fenix/search/SearchInteractor.kt rename to app/src/main/java/org/mozilla/fenix/search/SearchDialogInteractor.kt index 823945453..cd35bdbce 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogInteractor.kt @@ -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) { diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt deleted file mode 100644 index 7076dbf70..000000000 --- a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt +++ /dev/null @@ -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() - 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() - - 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(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(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, - 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(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 - } -} diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt index 74bad1b8c..46232e191 100644 --- a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt @@ -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), diff --git a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt index b00a82695..6f0719487 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -321,6 +321,8 @@ class SettingsFragment : PreferenceFragmentCompat() { val preferenceRemoteDebugging = findPreference(debuggingKey) val preferenceMakeDefaultBrowser = requirePreference(R.string.pref_key_make_default_browser) + val preferenceOpenLinksInExternalApp = + findPreference(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(getPreferenceKey(R.string.pref_key_override_fxa_server)) val preferenceSyncOverride = diff --git a/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragment.kt index a9f56e8b6..8ccc5ff87 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragment.kt @@ -169,25 +169,32 @@ class AccountSettingsFragment : PreferenceFragmentCompat() { updateSyncEngineStates() setDisabledWhileSyncing(accountManager.isSyncActive()) - requirePreference(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(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(it.prefId()).apply { + setOnPreferenceChangeListener { _, newValue -> + updateSyncEngineState(context, it, newValue as Boolean) + true + } } } - requirePreference(R.string.pref_key_sync_logins).apply { + // 'Passwords' listener is special, since we also display a pin protection warning. + requirePreference(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(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 diff --git a/app/src/main/java/org/mozilla/fenix/settings/account/SignOutFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/account/SignOutFragment.kt index f2c3ef861..c972737de 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/account/SignOutFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/account/SignOutFragment.kt @@ -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() diff --git a/app/src/main/java/org/mozilla/fenix/settings/account/TurnOnSyncFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/account/TurnOnSyncFragment.kt index 67ca920ed..fae824378 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/account/TurnOnSyncFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/account/TurnOnSyncFragment.kt @@ -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() { diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceView.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceView.kt index 49e313b57..c8106882b 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceView.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt index 2117f93fc..c65654fe0 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt @@ -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) + ) + ) } } diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareViewModel.kt b/app/src/main/java/org/mozilla/fenix/share/ShareViewModel.kt index 65d25caed..4fde40fce 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareViewModel.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareViewModel.kt @@ -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) diff --git a/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingObserver.kt b/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingObserver.kt index 10dd4132f..cc30b7421 100644 --- a/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingObserver.kt +++ b/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingObserver.kt @@ -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 } } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt index 431bfe898..41c6ddf40 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt @@ -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) + } } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt index 113a98c27..f97ef9055 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt @@ -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() } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt index 3cf89a386..556023572 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt @@ -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() + } } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt index 2d58a2bb8..4dc2a1ff9 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt index 285e4d454..2902b2392 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt @@ -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() } diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index 05bc08712..904f6956c 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -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) diff --git a/app/src/main/res/drawable/mozac_ic_warning_with_bottom_padding.xml b/app/src/main/res/drawable/mozac_ic_warning_with_bottom_padding.xml new file mode 100644 index 000000000..408f4c077 --- /dev/null +++ b/app/src/main/res/drawable/mozac_ic_warning_with_bottom_padding.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/component_tabhistory.xml b/app/src/main/res/layout/component_tabhistory.xml index 3095029a2..073753f5c 100644 --- a/app/src/main/res/layout/component_tabhistory.xml +++ b/app/src/main/res/layout/component_tabhistory.xml @@ -13,8 +13,8 @@ + + + + app:layout_constraintBottom_toTopOf="@id/infoBanner" /> @@ -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" /> + app:layout_constraintBottom_toTopOf="@id/divider" /> diff --git a/app/src/main/res/layout/component_tabs_screen_top.xml b/app/src/main/res/layout/component_tabs_screen_top.xml index 533254f94..bdd61d944 100644 --- a/app/src/main/res/layout/component_tabs_screen_top.xml +++ b/app/src/main/res/layout/component_tabs_screen_top.xml @@ -14,8 +14,8 @@ + + + + app:layout_constraintTop_toBottomOf="@id/infoBanner" /> + app:layout_constraintTop_toBottomOf="@id/handle"> @@ -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" /> + app:layout_constraintTop_toBottomOf="@id/divider" /> diff --git a/app/src/main/res/layout/component_tabstray_bottom.xml b/app/src/main/res/layout/component_tabstray_bottom.xml index f03df49b0..39dfcdb39 100644 --- a/app/src/main/res/layout/component_tabstray_bottom.xml +++ b/app/src/main/res/layout/component_tabstray_bottom.xml @@ -15,14 +15,23 @@ + + + + app:layout_constraintTop_toBottomOf="@id/infoBanner" /> + app:layout_constraintTop_toBottomOf="@id/handle"> @@ -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" /> + app:layout_constraintTop_toBottomOf="@id/divider" /> diff --git a/app/src/main/res/layout/component_tabstray_top.xml b/app/src/main/res/layout/component_tabstray_top.xml index f1439cd2d..3761136b4 100644 --- a/app/src/main/res/layout/component_tabstray_top.xml +++ b/app/src/main/res/layout/component_tabstray_top.xml @@ -23,6 +23,15 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintWidth_percent="0.1" /> + + + + app:layout_constraintBottom_toTopOf="@id/infoBanner" /> @@ -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" /> + app:layout_constraintBottom_toTopOf="@id/divider" /> diff --git a/app/src/main/res/layout/fragment_turn_on_sync.xml b/app/src/main/res/layout/fragment_turn_on_sync.xml index c933921d4..649aa056f 100644 --- a/app/src/main/res/layout/fragment_turn_on_sync.xml +++ b/app/src/main/res/layout/fragment_turn_on_sync.xml @@ -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"/> diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 1d3033460..67edefc7e 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -23,10 +23,6 @@ app:popUpTo="@id/homeFragment" app:popUpToInclusive="false" /> - - @@ -136,16 +132,11 @@ android:name="focusOnAddressBar" android:defaultValue="false" app:argType="boolean" /> - - - - - - - - دخلت وضع التحديد المتعدد، حدّد الألسنة لحفظها في تجميعة + + محدد + ‏%1$s من Mozilla. @@ -69,6 +72,11 @@ لا، شكرًا + + + افتح Firefox بسرعة أكبر. أضف أداة Firefox إلى الشاشة الرئيسية. + + أضف الأداة ليس الآن @@ -138,6 +146,8 @@ ثبّت الألسنة المُزامنة + + أعِد المزامنة ابحث في الصفحة @@ -155,10 +165,10 @@ افتح في %1$s - تدعمها %1$s + تدعمه %1$s - تدعمها %1$s + تدعمه %1$s منظور القارئ @@ -261,6 +271,8 @@ افتح الروابط في ألسنة خاصة اسمح بلقطات الشاشة في التصفّح الخاص + + إن سمحت فستظهر الألسنة الخاصة حين فتح أكثر من تطبيق أضِف اختصارًا للتصفح الخاص @@ -317,6 +329,8 @@ البحث في تأريخ التصفح البحث في العلامات + + ابحث في الألسنة المُزامنة إعدادات الحساب @@ -360,6 +374,11 @@ تاريخ آخر مزامنة: أبدًا + + ‏%1$s على %2$s %3$s + الألسنة المستلمة @@ -450,6 +469,11 @@ مرّر لإخفاء شريط الأدوات + + مرّر شريط الأدوات إلى اليمين واليسار للتبديل بين الألسنة + + مرّر شريط الأدوات إلى أعلى لفتح الألسنة + الجلسات @@ -690,6 +714,10 @@ أضِف مجلدًا اختر مجلدًا + + يجب أن يكون للعلامة عنوان + + مسار غير صحيح ما من علامات هنا فهمت + + تعذرت المشاركة مع هذا التطبيق أرسِل إلى جهاز @@ -839,8 +869,12 @@ جلسة تصفح خاصة حذف الألسنة الخاصة + + أغلِق الألسنة الخاصة افتح + + تدعمه حُذفت التجميعة @@ -944,9 +978,25 @@ صار Firefox Preview الآن Firefox Nightly + + تُحدّث النسخة الليلية من Firefox في كل ليلة وتُضاف إليها مزايا تجريبية جديدة. +ولكن قد يكون استقرارها أقل من العادية. نزّل متصفّح بيتا لتجربة أكثر استقرارًا. نزّل نسخة Firefox لأندرويد التجريبية + + انتقلت النسخة الليلية من Firefox من هنا + + لن يتلقى هذا التطبيق تحديثات الأمان بعد الآن. يُنصح بالتوقف عن استخدام هذا التطبيق والانتقال إلى النسخة الليلية الجديدة. +لنقل علاماتك وجلسات الولوج والتأريخ إلى تطبيق آخر، افتح حساب Firefox. + + انتقل إلى النسخة الليلية الجديدة + + + انتقلت النسخة الليلية من Firefox من هنا + + لن يتلقى هذا التطبيق تحديثات الأمان بعد الآن. ننصح بتنزيل النسخة الليلية الجديدة والتوقف عن استخدام هذا التطبيق. +لنقل علاماتك وجلسات الولوج والتأريخ إلى تطبيق آخر، افتح حساب Firefox. نزّل النسخة الليلية الجديدة @@ -961,6 +1011,21 @@ تعرف على %s اعرف ما الجديد + + ألديك أسئلة عن متصفّح %s الذي أعدنا تصميمه؟ أتريد معرفة ما تغيّر؟ + + ستجد هنا إجابات أسئلتك + + اشرع الآن بمزامنة العلامات وكلمات السر وغيرها الكثير عبر حساب Firefox. + + اطّلع على المزيد + + ولجت ببريد %s على متصفّح Firefox آخر على هذا الجهاز. أتريد الولوج بنفس الحساب؟ + + نعم، سألج بنفس الحساب تَلج الآن… @@ -975,6 +1040,10 @@ صارم (مستحسن) صارم + + اختر جهةً تصفّح بخصوصية @@ -1312,7 +1381,7 @@ حسنًا، فهمت - استغلّ %s إلى أقصى حد. diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index c471c14d6..17d3923fd 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -282,6 +282,8 @@ Тэма Хатняя старонка + + Жэсты Уладкаванне @@ -317,8 +319,12 @@ Пошук у гісторыі аглядання Пошук у закладках + + Шукаць у сінхранізаваных картках Налады ўліковага запісу + + Аўтазапаўненне URL-адрасоў Адкрываць спасылкі ў праграмах @@ -450,6 +456,10 @@ Тэма прылады + + + Пацягніце, каб абнавіць + Сеансы diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 701f63e17..5c471e441 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -1162,6 +1162,8 @@ Fodd bynnag, gall fod yn llai sefydlog. Llwythwch ein porwr Beta i gael profiad Mewngofnodi gyda’ch camera Defnyddiwch e-bost yn lle hynny + + Crëwch un i gydweddu Firefox rhwng dyfeisiau.]]> Bydd Firefox yn peidio cydweddu eich cyfrif ond ni fydd yn dileu eich data pori ar y ddyfais hon. @@ -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 --> %s| Llyfrgelloedd OSS + + Ailgyfeirio Tracwyr + + Yn clirio cwcis wedi’u gosod i ailgyfeirio i wefannau tracio hysbys. + Cefnogaeth diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7ead58dc5..6e06d58e6 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1197,6 +1197,8 @@ Melden Sie sich mit Ihrer Kamera an Stattdessen E-Mail-Adresse verwenden + + Erstellen Sie eines, um Firefox zwischen Geräten zu synchronisieren.]]> Firefox beendet die Synchronisation mit Ihrem Konto, löscht aber keine Surf-Daten auf diesem Gerät. @@ -1304,6 +1306,11 @@ The first parameter is the app name --> %s | OSS-Bibliotheken + + Weiterleitungs-Tracker + + Löscht Cookies, die durch Weiterleitungen zu bekannten Tracking-Websites gesetzt wurden. + Hilfe diff --git a/app/src/main/res/values-dsb/strings.xml b/app/src/main/res/values-dsb/strings.xml index 26e9371a7..7aa0153e7 100644 --- a/app/src/main/res/values-dsb/strings.xml +++ b/app/src/main/res/values-dsb/strings.xml @@ -1164,6 +1164,8 @@ Pśizjawśo se ze swójeju kameru E-mail město togo wužywaś + + Załožćo take, aby Firefox mjazy rědami synchronizěrował.]]> Firefox pśestanjo z wašym kontom synchronizěrowaś, ale njewulašujo pśeglědowańske daty na toś tom rěźe. @@ -1274,6 +1276,11 @@ The first parameter is the app name --> %s | OSS-biblioteki + + Dalejpósrědnjańske pśeslědowaki + + Wulašujo cookije, kótarež su se stajili pśez dalejpósrědnjenja k znatym slědujucym websedłam. + Pomoc diff --git a/app/src/main/res/values-en-rCA/strings.xml b/app/src/main/res/values-en-rCA/strings.xml index b9f8406b4..b0c01c48d 100644 --- a/app/src/main/res/values-en-rCA/strings.xml +++ b/app/src/main/res/values-en-rCA/strings.xml @@ -1155,6 +1155,8 @@ Sign in with your camera Use email instead + + Create one to sync Firefox between devices.]]> Firefox will stop syncing with your account, but won’t delete any of your browsing data on this device. @@ -1262,6 +1264,11 @@ The first parameter is the app name --> %s | OSS Libraries + + Redirect Trackers + + Clears cookies set by redirects to known tracking websites. + Support diff --git a/app/src/main/res/values-es-rAR/strings.xml b/app/src/main/res/values-es-rAR/strings.xml index 9ac54abc6..9d6aaf1d0 100644 --- a/app/src/main/res/values-es-rAR/strings.xml +++ b/app/src/main/res/values-es-rAR/strings.xml @@ -1182,6 +1182,8 @@ Inicia sesión con tu cámara Use el correo electrónico en su lugar + + Creá una para sincronizar Firefox entre dispositivos.]]> Firefox va a dejar de sincronizar con tu cuenta pero no va a eliminar ningún dato de navegación en este dispositivo. @@ -1290,6 +1292,11 @@ The first parameter is the app name --> %s | Bibliotecas OSS + + Redirigir rastreadores + + Borra las cookies establecidas por redireccionamientos a sitios web de rastreo conocidos. + Ayuda diff --git a/app/src/main/res/values-es-rCL/strings.xml b/app/src/main/res/values-es-rCL/strings.xml index 829553b6b..bafbf9d67 100644 --- a/app/src/main/res/values-es-rCL/strings.xml +++ b/app/src/main/res/values-es-rCL/strings.xml @@ -144,6 +144,8 @@ Instalar Pestañas sincronizadas + + Resincronizar Buscar en la página @@ -1159,6 +1161,8 @@ Conéctate con tu cámara O usa tu correo + + Crea una para sincronizar Firefox entre dispositivos.]]> Firefox dejará de sincronizarse con tu cuenta, pero no se borrarán los datos de navegación del dispositivo. @@ -1266,6 +1270,11 @@ The first parameter is the app name --> %s | Bibliotecas OSS + + Rastreadores de redirección + + Limpia las cookies creadas por redirecciones a sitios web de seguimiento conocidos. + Ayuda diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 2c59ac64c..1e0f6c3ab 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -1178,6 +1178,8 @@ Kirjaudu sisään kamerallasi Käytä sähköpostia + + Luo tili synkronoidaksesi Firefox laitteiden välillä.]]> Firefox lopettaa tilisi synkronoinnin, mutta ei poista mitään selaustietoja tältä laitteelta. @@ -1286,6 +1288,11 @@ The first parameter is the app name --> %s | Avoimen lähdekoodin kirjastot + + Uudelleenohjausseuraimet + + Tyhjentää evästeet, jotka on asetettu tunnetuille seurantasivustoille johtavilla uudelleenohjauksilla. + Tuki diff --git a/app/src/main/res/values-gn/strings.xml b/app/src/main/res/values-gn/strings.xml index 61e9ebc74..a5a52a46e 100644 --- a/app/src/main/res/values-gn/strings.xml +++ b/app/src/main/res/values-gn/strings.xml @@ -1180,6 +1180,8 @@ Eñepyrũ tembiapo ne ra’ãngamýi ndive Eipuru ñandutiveve + + Emoheñói embojuehe hag̃ua Firefox mba’e’oka ndive.]]> Firefox nombojuehemo’ãvéima ne mba’ete, hákatu nomboguemo’ãi ne kundahára mba’ekuaarã ko mba’e’oka pegua. @@ -1292,6 +1294,11 @@ The first parameter is the app name --> %s | OSS Arandukarenda + + Embohapejey tapykuehoha + + Embogue umi kookie oñembohapejeýva ñanduti renda rapykuehohápe. + Ñepytyvõ diff --git a/app/src/main/res/values-hsb/strings.xml b/app/src/main/res/values-hsb/strings.xml index 4baecf054..c1867b61b 100644 --- a/app/src/main/res/values-hsb/strings.xml +++ b/app/src/main/res/values-hsb/strings.xml @@ -1166,6 +1166,8 @@ Přizjewće so ze swojej kameru E-mejl město toho wužiwać + + Załožće tajke, zo byšće Firefox mjez gratami synchronizował.]]> Firefox přestanje z wašim kontom synchronizować, ale njezhaša přehladowanske daty na tutym graće. @@ -1273,6 +1275,11 @@ The first parameter is the app name --> %s | OSS-biblioteki + + Sposrědkowanske přesćěhowaki + + Zhaša placki, kotrež su so přez dalesposrědkowanja k znatym slědowacym websydłam stajili. + Pomoc diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index dac694860..aa8193f44 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -1174,6 +1174,8 @@ Jelentkezzen be a kamerájával E-mail használata ehelyett + + Hozzon létre egyet, hogy szinkronizálja a Firefoxot az eszközök között.]]> 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. @@ -1283,6 +1285,11 @@ The first parameter is the app name --> %s | Nyílt forráskódú programkönyvtárak + + Nyomkövetők átirányítása + + Törli az ismert nyomkövető webhelyekre történő átirányítással beállított sütiket. + Támogatás diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 41eeb1c36..1f039649a 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -1157,6 +1157,8 @@ התחברות באמצעות המצלמה שלך שימוש בדוא״ל במקום + + באפשרותך ליצור אחד כדי לסנכרן את Firefox בין מכשירים.]]> ‏Firefox יפסיק להסתנכרן עם החשבון שלך, אבל לא ימחק את נתוני הגלישה שלך ממכשיר זה. @@ -1230,7 +1232,7 @@ עוגיות מעקב חוצות אתרים - חסימת עוגיות שרשתות פרסומות וחברות ניתוח תעבורה משתמשות בהן כדי לאסוף פרופיל על נתוני הגלישה שלך על פני מגוון רחב של אתרים. + חוסם עוגיות שרשתות פרסומות וחברות ניתוח תעבורה משתמשות בהן כדי לאסוף פרופיל על נתוני הגלישה שלך על פני מגוון רחב של אתרים. כורי מטבעות דיגיטליים @@ -1265,6 +1267,11 @@ The first parameter is the app name --> ‏%s | ספריות OSS + + רכיבי מעקב של הפניות + + מנקה עוגיות המוגדרות על־ידי הפניות לאתרי מעקב ידועים. + תמיכה @@ -1279,6 +1286,9 @@ ספריות בהן אנו משתמשים + + תפריט ניפוי שגיאות: נותרו %1$d לחיצות כדי להפעיל תפריט ניפוי שגיאות מופעל diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index 40ffa6f2a..47bd23666 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -48,7 +48,7 @@ მონიშნულია - %1$s შექმნა @fork-maintainers-მ. + %1$s შემქმნელი @fork-maintainers. @@ -143,6 +143,8 @@ დაყენება დასინქრონებული ჩანართები + + კვლავ დასინქრონება პოვნა გვერდზე @@ -325,6 +327,8 @@ ძიება დათვალიერების ისტორიაში სანიშნების ძიება + + დასინქრონებული ჩანართების მოძიება ანგარიშის პარამეტრები @@ -1234,7 +1238,7 @@ საიტთაშორისი მეთვალყურე ფუნთუშები - ზღუდავს ფუნთუშებს, რომლებსაც სარეკლამო და ანალიტიკური კომპანიები იყენებენ თქვენზე მონაცემების ერთიანად აღრიცხვისთვის ბევრ საიტზე. + ზღუდავს ფუნთუშებს, რომლებსაც სარეკლამო და ანალიტიკური კომპანიები იყენებენ თქვენზე მონაცემების ერთიანად შეგროვებისთვის, სხვადასხვა საიტიდან. კრიპტოგამომმუშავებლები @@ -1405,7 +1409,7 @@ დაიცავით თქვენი ანგარიშები - დააყენეთ მოწყობილობის ჩასაკეტად მოსახაზი, PIN-კოდი ან პაროლი, თქვენი შენახული ანგარიშების მონაცემებთან, უცხო პირების წვდომის აღსაკვეთათ. + დააყენეთ მოწყობილობის ჩასაკეტად მოსახაზი, PIN-კოდი ან პაროლი, თქვენი შენახული ანგარიშების მონაცემებთან, უცხო პირების წვდომის აღსაკვეთად. მოგვიანებით diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml index 8601523ce..ce31fcdc9 100644 --- a/app/src/main/res/values-kk/strings.xml +++ b/app/src/main/res/values-kk/strings.xml @@ -1155,6 +1155,8 @@ Камерамен кіріңіз Оның орнына эл. поштаны пайдалану + + тіркелгі жасаңыз.]]> Firefox тіркелгіңізбен синхрондауды тоқтатады, бірақ, бұл құрылғыда барлық шолу деректері қалады. @@ -1267,6 +1269,11 @@ The first parameter is the app name --> %s | Ашық библиотекалар + + Қайта бағыттайтын трекерлер + + Белгілі бақылайтын веб-сайттарға қайта бағыттау арқылы орнатылған cookie файлдарын тазарту. + Қолдау diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index b47cab6ec..42114c5f5 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1210,6 +1210,8 @@ 카메라로 로그인 대신 이메일 사용 + + 하나를 만드세요.]]> Firefox가 계정과의 동기화를 중단하지만 이 기기의 사용자 탐색 데이터는 삭제하지 않습니다. @@ -1319,6 +1321,12 @@ The first parameter is the app name --> %s | OSS 라이브러리 + + 트래커 리디렉션 + + + 알려진 추적 웹 사이트로 리디렉션하여 설정된 쿠키를 지웁니다. + 지원 diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 37d78a947..1561b0a51 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -1178,6 +1178,8 @@ Logg inn med kameraet ditt Bruk e-post i stedet + + Opprett en for å synkronisere Firefox mellom enheter.]]> Firefox vil stoppe synkroniseringen med kontoen din, men vil ikke slette noen av dine nettleserdata på denne enheten. @@ -1290,6 +1292,11 @@ The first parameter is the app name --> %s | OSS-bibliotek + + Omdirigeringssporere + + Fjerner infokapsler satt av omdirigeringer til kjente sporingsnettsteder. + Brukerstøtte diff --git a/app/src/main/res/values-nn-rNO/strings.xml b/app/src/main/res/values-nn-rNO/strings.xml index 746e17f9d..0a2a11e2e 100644 --- a/app/src/main/res/values-nn-rNO/strings.xml +++ b/app/src/main/res/values-nn-rNO/strings.xml @@ -1176,6 +1176,8 @@ Logg inn med kameraet ditt Bruk e-post i staden + + Lag ein for å synkronisere Firefox mellom einingar.]]> Firefox vil stoppe synkroniseringa med kontoen din, men vil ikkje slette nettleserdataa dine på denne eininga. diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index 9a014ca6c..a6b4468a0 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -139,6 +139,8 @@ Installar Onglets sincronizats + + Tornar sincronizar Recercar dins la pagina diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index df7ff3f34..cf01e0e90 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1163,6 +1163,8 @@ Entre com sua câmera Usar e-mail + + Crie uma para sincronizar o Firefox entre dispositivos.]]> O Firefox deixará de sincronizar com sua conta, mas não excluirá seus dados de navegação neste dispositivo. @@ -1274,6 +1276,11 @@ The first parameter is the app name --> %s | Bibliotecas de código aberto + + Rastreadores de redirecionamento + + Limpa cookies definidos por redirecionamento de sites de rastreamento conhecidos. + Suporte diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 5e300e3e3..78f407da7 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -146,6 +146,8 @@ Namesti Sinhronizirani zavihki + + Znova sinhroniziraj Najdi na strani @@ -268,6 +270,8 @@ Odpri povezave v zasebnem zavihku Dovoli zajemanje posnetkov zaslona v zasebnem brskanju + + Če je dovoljeno, bodo zasebni zavihki vidni tudi, ko je odprtih več aplikacij Dodaj bližnjico zasebnega brskanja @@ -328,6 +332,8 @@ Nastavitve računa + + Samodokončaj spletne naslove Odpiraj povezave v aplikacijah diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 19281efbe..3be1e8db4 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -1181,6 +1181,8 @@ Logga in med din kamera Använd e-post istället + + Skapa ett för att synkronisera Firefox mellan enheter.]]> Firefox kommer sluta att synka med ditt konto, men kommer inte att radera din surfdata på den här enheten. @@ -1289,6 +1291,11 @@ The first parameter is the app name --> %s | OSS bibliotek + + Omdirigera spårare + + Rensar kakor som ställts in av omdirigeringar till kända spårningswebbplatser. + Support diff --git a/app/src/main/res/values-tg/strings.xml b/app/src/main/res/values-tg/strings.xml new file mode 100644 index 000000000..f7b2b6379 --- /dev/null +++ b/app/src/main/res/values-tg/strings.xml @@ -0,0 +1,1151 @@ + + + + %s-и махфӣ + + %s (Махфӣ) + + + Имконоти бештар + + Фаъол кардан баррасии махфӣ + + Ғайрифаъол кардан баррасии махфӣ + + Нишониеро ҷустуҷӯ кунед ё ворид намоед + + Варақаҳои кушодашудаи шумо дар ин ҷо нишон дода мешаванд. + + Варақаҳои махфии шумо дар ин ҷо нишон дода мешаванд. + + + 1 варақаи кушодашуда. Барои гузариш байни варақаҳо зарба занед. + + %1$s варақаи кушодашуда. Барои гузариш байни варақаҳо зарба занед. + + %1$d интихоб шуд + + Илова кардани маҷмӯаи нав + + Ном + + Маҷмӯаро интихоб кунед + + Аз реҷаи серинтихоб баромадан + + + Нигоҳ доштани варақаҳои интихобшуда дар маҷмӯа + + %1$s интихоб карда шуд + + Интихоби %1$s бекор карда шуд + + + Аз реҷаи серинтихоб баромад + + Реҷаи серинтихоб фаъол шуд, варақаҳоеро барои нигоҳ доштан дар маҷмӯа интихоб намоед + + Интихоб шуд + + + %1$s аз ҷониби Mozilla истеҳсол карда шудааст. + + + + Шумо дар реҷаи махфӣ қарор доред + + Нест кардани ҷаласа + + + + Барои кушодани варақаҳои махфӣ аз экрани асосӣ миёнбуреро илова кунед. + + Илова кардани миёнбур + + Не, ташаккур + + + Илова кардани виҷет + + Ҳоло не + + + Гузариш ба танзимот + + Нодида гузарондан + + + Гузариш ба танзимот + + + Нодида гузарондан + + + Имконоти намоиш + + Нодида гузарондан + + + + Варақаи нав + + Варақаи махфии нав + + Сомонаҳои беҳтарин + + + + Варақаҳои кушодашуда + + Бозгашт + + Ба пеш + + Нав кардан + + Истодан + + Хатбарак + + Таҳрир кардани хатбарак + + Ҷузъҳои иловагӣ + + Дар ин ҷо ягон ҷузъи иловагӣ нест + + Кумак + + Чӣ нав аст + + Танзимот + + Китобхона + + + Барои компютери мизи корӣ + + Илова кардан ба экрани асосӣ + + Насб кардан + + Варақаҳои ҳамоҳангшуда + + Аз нав ҳамоҳанг кардан + + Ҷустуҷӯ дар саҳифа + + Варақаи махфӣ + + Варақаи нав + + Нигоҳ доштан дар маҷмӯа + + Мубодила кардан + + Мубодила кардан тавассути… + + Кушодан дар %1$s + + ДАР %1$s АСОС МЕЁБАД + + Дар %1$s асос меёбад + + Намоиши хониш + + Пӯшидани намоиши хониш + + Кушодан дар барнома + + Намуди зоҳирӣ + + + Пайваст нашуд. Нақшаи URL шинохтанашаванда аст. + + + + Забони интихобшуда + + Ҷустуҷӯ + + Забон аз дастгоҳ асос меёбад + + Ҷустуҷӯи забон + + + + Ҷустуҷӯ + + Низоми ҷустуҷӯӣ + + Танзимоти низоми ҷустуҷӯӣ + + Ин дафъа бо зерин ҷустуҷӯ кунед: + + Гузоштани пайванд аз ҳофизаи муваққатӣ + + Иҷозат додан + + Иҷозат дода нашавад + + Маълумоти бештар + + + + Ҷустуҷӯ + + Ҷустуҷӯ дар Интернет + + Ҷустуҷӯи овозӣ + + + + Танзимот + + Асосҳо + + Умумӣ + + Дар бораи барнома + + Низоми ҷустуҷӯии пешфарз + + Ҷустуҷӯ + + Лавҳаи нишонӣ + + Кумак + + Баҳодиҳӣ дар Google Play + + Изҳори назари худро пешниҳод кунед + + Дар бораи %1$s + + Ҳуқуқҳои шумо + + Ниҳонвожаҳо + + + Кортҳои қарзӣ ва нишониҳо + + Гузоштан ҳамчун браузери пешфарз + + Иловагӣ + + Махфият + + Махфият ва амният + + Иҷозатҳои сомона + + Баррасии махфӣ + + Кушодани пайвандҳо дар варақаи махфӣ + + Қобилияти дастрасӣ + + Ҳисоб + + Ворид шудан + + Навори абзорҳо + + Мавзӯъ + + Асосӣ + + Ишораҳо + + Фармоишдиҳӣ + + Ҳисоби Firefox + + Барои барқарор кардани ҳамоҳангсозӣ аз нав пайваст шавед + + Забон + + Интихоби маълумот + + Маҷмӯаи маълумот + + Огоҳиномаи махфият + + Абзорҳои барномасозӣ + + Намоиш додани низомҳои ҷустуҷӯӣ + + Намоиш додани пешниҳодҳои ҷустуҷӯ + + Намоиш додани ҷустуҷӯи овозӣ + + Намоиш додан дар ҷаласаҳои махфӣ + + Намоиш додани пешниҳодҳо аз ҳофизаи муваққатӣ + + Намоиш додани таърихи тамошо + + Ҷустуҷӯи хатбаракҳо + + Ҷустуҷӯи варақаҳои ҳамоҳангшуда + + Танзимоти ҳисоб + + Пуркунии худкори нишонаҳои URL + + Кушодани пайвандҳо дар барномаҳо + + Мудири берунии боргириҳо + + Ҷузъҳои иловагӣ + + Огоҳиномаҳо + + + + Ҳозир ҳамоҳанг кунед + + Интихоб кунед, ки чӣ ҳамоҳанг карда мешавад + + Таърих + + Хатбаракҳо + + Воридшавиҳо + + Варақаҳои кушодашуда + + Баромад + + + Номи дастгоҳ + + Номи дастгоҳ наметавонад холӣ бошад. + + Ҳамоҳангсозӣ… + + + %1$s дар %2$s %3$s + + + + Варақаҳо аз дастгоҳҳои дигар + + Огоҳиномаҳо дар бораи варақаҳое, ки аз дигар дастгоҳҳои Firefox гирифта шудаанд. + + Рарақаи қабулшуда + + Рарақаҳои қабулшуда + + Варақа аз %s + + + + Муҳофизат аз пайгирӣ + + Муҳофизат аз пайгирӣ + + Муҳтаво ва скриптҳоеро, ки шуморо онлайн пайгирӣ мекунанд, маҳдуд кунед + + Истисноҳо + + Муҳофизат аз пайгирӣ барои ин сомонаҳо хомӯш аст + + Фаъол кардани барои ҳамаи сомонаҳо + + Истисноҳо ба шумо имкон медиҳанд, ки муҳофизат аз пайгириро барои сомонаҳои интихобшуда ғайрифаъол кунед. + + Маълумоти бештар + + + Комилан ғайрифаъол карда шуд, Барои фаъол кардани он ба Танзимот гузаред. + + + Телеметрия + + Истифодабарӣ ва маълумоти техникӣ + + Маълумоти маркетингӣ + + Таҷрибаҳо + + Хадамоти ҷойгиршавии Mozilla + + Гузориши саломатии %s + + + + Фаъол кардани ҳамоҳангсозӣ + + Ворид шудан + + Барои аз нав пайваст шудан ворид шавед + + Барҳам додани ҳисоб + + + Кушодани камера + + Бекор кардан + + + + Боло + + Поён + + + + Равшан + + Торик + + Аз ҷониби сарфаи батарея муқаррар карда шудааст + + Дар мавзӯи дастгоҳ асос меёбад + + + + Барон нав кардан кашед + + Барои пинҳон кардани навори абзорҳо ҳаракат кунед + + + + Ҷаласаҳо + + Аксҳои экран + + Боргириҳо + + Хатбаракҳо + + Хатбаракҳо дар компютери мизи корӣ + + Менюи хатбаракҳо + + Навори хатбаракҳо + + Хатбаракҳои дигар + + Таърих + + Варақаҳои ҳамоҳангшуда + + Рӯйхати хониш + + Ҷустуҷӯ + + Танзимот + + Менюи унсури таърих + + Пӯшидан + + + Намоиш додани таърихи пурра + + %d варақа + + %d варақа + + + + Пӯшидани варақаҳо + + Ба таври дастӣ + + Пас аз як рӯз + + Пас аз як ҳафта + + Пас аз як моҳ + + + + Варақаҳои кушодашуда + + + Ҷаласаи махфӣ + + Варақаҳои махфӣ + + Илова кардани варақа + + Илова кардани варақаи махфӣ + + Махфӣ + + Варақаҳои кушодашуда + + Нигоҳ доштан дар маҷмӯа + + Мубодила кардани ҳамаи варақаҳо + + Танзимоти варақа + + Пӯшидани ҳамаи варақаҳо + + Варақаи нав + + Гузариш ба саҳифаи асосӣ + + Пӯшидани варақа + + Пӯшидани варақаи %s + + Менюи варақаҳои кушодашуда + + Пӯшидани ҳамаи варақаҳо + + Мубодила кардани варақаҳо + + Нигоҳ доштани варақаҳо дар маҷмӯа + + Менюи варақаҳо + + Мубодила кардани варақа + + Нест кардан + + Нигоҳ доштан + + Мубодила кардан + + Тасвири ҷаласаи ҷорӣ + + Нигоҳ доштан дар маҷмӯа + + Нест кардани маҷмӯа + + Иваз кардани номи маҷмӯа + + Варақаҳои кушодашуда + + Тоза кардан + + Нест кардан аз таърих + + %1$s (Реҷаи махфӣ) + + Нигоҳ доштан + + + + Нест кардани таърих + + Шумо мутмаин ҳастед, ки мехоҳед таърихи худро нест намоед? + + Таърих нест карда шуд + + %1$s нест карда шуд + + Пок кардан + + Нусха бардоштан + + Мубодила кардан + + Кушодан дар варақаи нав + + Кушодан дар варақаи махфӣ + + Нест кардан + + %1$d интихоб шуд + + Нест кардани %1$d ҷузъ + + 24 соати охир + + 7 рӯзи охир + + 30 рӯзи охир + + Пештар + + Ягон таърих нест + + + + Ягон боргирӣ нест + + %1$d интихоб шуд + + + + Мутаассифона, %1$s ин саҳифаро бор карда наметавонад. + + + Шумо метавонед ин варақаро дар поён барқарор кунед ё пӯшед. + + Фиристодани гузориш дар бораи садама ба Mozilla + + Пӯшидани варақа + + Барқарор кардани варақа + + + Имконоти ҷаласа + + + Мубодила кардани ҷаласа + + + + Менюи хатбаракҳо + + Таҳрир кардани хатбарак + + Интихоб кардани ҷузвадон + + Шумо мутмаин ҳастед, ки мехоҳед ин ҷузвадонро нест намоед? + + %s ҷузъҳои интихобшударо нест мекунад. + + %1$s нест карда шуд + + Илова кардани ҷузвадон + + Хатбарак эҷод карда шуд. + + Хатбарак нигоҳ дошта шуд! + + ТАҲРИР КАРДАН + + Таҳрир кардан + + Интихоб кардан + + Нусха бардоштан + + Мубодила кардан + + Кушодан дар варақаи нав + + Кушодан дар варақаи махфӣ + + Нест кардан + + Нигоҳ доштан + + %1$d интихоб шуд + + Таҳрир кардани хатбарак + + Таҳрир кардани ҷузвадон + + Барои дидани хатбаракҳои ҳамоҳангшуда ворид шавед + + Нишонии URL + + ҶУЗВАДОН + + НОМ + + Илова кардани ҷузвадон + + Интихоб кардани ҷузвадон + + Бояд унвон дошта бошад + + Нишонии URL беэътибор аст + + Ягон хатбарак нест + + %1$s нест карда шуд + + Хатбаракҳо нест карда шуданд + + + + Иҷозатҳо + + Гузариш ба Танзимот + + Тавсияшуда + + Идоракунии иҷозатҳои сомона + + Пок кардани иҷозатҳо + + Пок кардани иҷозат + + Пок кардани иҷозатҳо дар ҳамаи сомонаҳо + + Пахши худкор + + Камера + + Микрофон + + Ҷойгиршавӣ + + + Огоҳинома + + Бояд манъ карда шавад + + Бояд иҷозат дода шавад + + Аз тарафи Android манъ карда шуд + + Истисноҳо + + Фаъол + + Ғайрифаъол + + + Иҷозат додани аудио ва видео + + Фаъол + + Ғайрифаъол + + + + Маҷмӯаҳо + + Менюи маҷмӯаҳо + + Пӯшидан + + Нигоҳ доштан + + Намоиш + + + Маҷмӯаи %d + + + + Фиристодан ва мубодила кардан + + Мубодила кардан + + Мубодила кардан + + Мубодила кардани пайванд + + Фиристодан ба дастгоҳ + + Ҳамаи амалҳо + + Барои ҳамоҳангсозӣ ворид шавед + + Фиристодан ба ҳамаи дастгоҳҳо + + Барои ҳамоҳангсозӣ аз нав пайваст шавед + + Офлайн + + Фаҳмо + + + + Ҷаласаи баррасии махфӣ + + Нест кардани варақаҳои махфӣ + + Пӯшидани варақаҳои махфӣ + + Кушодан + + Нест кардан ва кушодан + + Дар асоси + + Маҷмӯа нест карда шуд + + Номи маҷмӯа иваз карда шуд + + Варақа нест карда шуд + + Варақаҳо нест карда шуданд + + Варақа пӯшида шуд + + Варақаҳо пӯшида шуданд + + Ба сомонаҳои беҳтарин илова карда шуд! + + Варақаи махфӣ пӯшида шуд + + Варақаҳои махфӣ пӯшида шуданд + + Варақаҳои махфӣ нест карда шуданд + + Сомона хориҷ карда шуд + + Тасдиқ кардан + + Ба %1$s иҷозат диҳед, ки %2$s-ро кушояд + + ИҶОЗАТ ДОДАН + + РАД КАРДАН + + Шумо мутмаин ҳастед, ки мехоҳед %1$s-ро нест намоед? + + %1$s-ро нест мекунед? + + Нест кардан + + Бекор кардан + + Ба реҷаи экрани пурра ворид шуда истодааст + + URL нусха бардошта шуд + + Андозаи ҳуруф + + + Андозагирии худкори ҳуруф + + + Нест кардани маълумоти баррасӣ + + Варақаҳои кушодашуда + + %d варақа + + Таърих + + %d саҳифа + + Кукиҳо + + + Бекор кардан + + Нест кардан + + + + Хуш омадед ба %s! + + Маълумоти бештар + + Ҳамоҳангсозӣ фаъол аст + + Воридшавӣ иҷро нашуд + + Махфияти худкор + + Кушодани танзимот + + Махфияти шумо + + Огоҳиномаи махфияти моро хонед + + Пӯшидан + + + Оғоз кардани баррасӣ + + + + Мавзӯи худро интихоб кунед + + Худкор + + Ба танзимоти дастгоҳи шумо мутобиқат мекунад + + Мавзӯи торик + + Мавзӯи равшан + + + Варақаҳо фиристода шуданд! + + Варақа фиристода шуд! + + Ирсол ғайриимкон аст + + АЗ НАВ КӮШИШ КАРДАН + + Қатъ кардани пайваст + + Бекор кардан + + Ҷузвдонҳои пешфарзро таҳрир карда наметавонад + + + + Танзимоти муҳофизат + + Муҳофизати такмилёфта аз пайгирӣ + + Маълумоти бештар + + Стандартӣ (пешфарз) + + Фармоишӣ + + + Кукиҳо + + Муҳтавои пайгирикунанда + + Дар ҳамаи варақаҳо + + Танҳо дар варақаҳои махфӣ + + Танҳо дар варақаҳои фармоишӣ + + Криптомайнерҳо + + Хонандаи изи ангушт + Манъ карда мешавад + + Иҷозат дода мешавад + + Криптомайнерҳо + + Хонандаи изи ангушт + + Муҳтавои пайгирикунанда + + Ҳуқуқҳои шумо + + Китобхонаҳо бо манбаи кушоде, ки мо истифода мебарем + + Дар %s чӣ нав аст + + %s | Китобхонаҳои OSS + + + Дастгирӣ + + Садамот + + Огоҳиномаи махфият + + Ҳуқуқҳои худро донед + + Маълумот дар бораи иҷозатнома + + Китобхонаҳое, ки мо истифода мебарем + + + 1 варақа + + %d варақа + + + + Нусха бардоштан + + Нусха бардоштан ва гузаштан + + Гузоштан + + URL ба ҳофизаи муваққатӣ нусха бардошта шуд + + + Илова кардан ба экрани асосӣ + + Бекор кардан + + Илова кардан + + Ба сомона идома диҳед + + Номи миёнбур + + + Воридшавиҳо ва ниҳонвожаҳо + + Нигоҳ доштани воридшавиҳо ва ниҳонвожаҳо + + Ҳеҷ гоҳ нигоҳ дошта нашавад + + Пуркунии худкор + + Воридшавиҳои ҳамоҳангшуда + + Фаъол + + Ғайрифаъол + + Аз нав пайваст кардан + + Барои ҳамоҳангсозӣ ворид шавед + + Воридшавиҳои нигоҳдошташуда + + Маълумоти бештар дар бораи ҳамоҳангсозӣ + + Истисноҳо + + Нест кардани ҳамаи истисноҳо + + Ҷустуҷӯи воридшавиҳо + + Аз рӯи алифбо + + Сомона + + Номи корбар + + Ниҳонвожа + + PIN-и худро такроран ворид намоед + + Маълумоти бештар + + Шумо мехоҳед, ки %s воридшавии шуморо нигоҳ дорад? + + Нигоҳ доштан + + Нигоҳ дошта нашавад + + Ниҳонвожа ба ҳофизаи муваққатӣ нусха бардошта шуд + + Номи корбар ба ҳофизаи муваққатӣ нусха бардошта шуд + + Сомона ба ҳофизаи муваққатӣ нусха бардошта шуд + + Нусха бардоштани ниҳонвожа + + Пок кардани ниҳонвожа + + Нусха бардоштани номи корбар + + Пок кардани номи корбар + + Нусха бардоштани сомона + + Кушодани сомона дар браузер + + Нишон додани ниҳонвожа + + Пинҳон кардани ниҳонвожа + + Дертар + + Ҳозир насб кунед + + Қулфи дастгоҳи худро кушоед + + Тағйири андоза дар ҳамаи сомонаҳо + + Ном (А-Я) + + + Илова кардани низоми ҷустуҷӯӣ + + Таҳрир кардани низоми ҷустуҷӯӣ + + Илова кардан + + Нигоҳ доштан + + Таҳрир кардан + + Нест кардан + + + Дигар + + Ном + + Маълумоти бештар + + Пайванди «Маълумоти бештар» + + + %s эҷод карда шуд + + %s нигоҳ дошта шуд + + %s нест карда шуд + + + Оғоз кардани %s + + Интиқол анҷом ёфт + + Ниҳонвожаҳо + + + Барои иҷозат додан: + + 1. Ба Танзимоти Android гузаред + + Иҷозатҳо-ро зер кунед]]> + + %1$s ба ФАЪОЛ иваз намоед]]> + + + Пайвасти боэътимод + + Пайвасти беэътимод + + Мақолаҳои беҳтарин + + Илова кардан ба сомонаҳои беҳтарин + + Нест кардан + + Таҳрир кардан + + Шумо мутмаин ҳастед, ки мехоҳед ин воридшавиро нест намоед? + + Нест кардан + + Имконоти воридшавӣ + + Нигоҳ доштани тағйирот барои воридшавӣ + + Рад кардани тағйирот + + Таҳрир кардан + + + Ниҳонвожа лозим аст + + Ҷустуҷӯи овозӣ + + Акнун ҳарф занед + + + Воридшавӣ бо ин номи корбар аллакай вуҷуд дорад + + + + Дастгоҳи дигареро пайваст кунед. + + + Лутфан, санҷиши ҳаққониятро аз нав такрор кунед + + Лутфан ҳамоҳангсозии варақаҳоро фаъол кунед. + + Ягон варақаи кушодашуда нест + + + Хуб, фаҳмидам + + + Тоза кардан + + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index c1c228dfa..c80ec3e5c 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1162,6 +1162,8 @@ Kameranızla giriş yapın E-posta ile giriş yap + + hesap açın.]]> Firefox artık hesabınızla eşitlenmeyecek ama bu cihazdaki gezinti geçmişiniz silinmeyecek. @@ -1270,6 +1272,11 @@ The first parameter is the app name --> %s | OSS Kitaplıkları + + Yönlendirme takipçileri + + Bilinen takip sitelerine yapılan yönlendirmelere ait çerezleri temizler. + Destek diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index e9c83525d..0ace60b00 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1176,6 +1176,8 @@ Використати е-пошту + + Створіть його для синхронізації Firefox між пристроями.]]> Firefox припинить синхронізацію з вашим обліковим записом, але не вилучить жодних даних перегляду на цьому пристрої. @@ -1284,6 +1286,11 @@ The first parameter is the app name --> %s | Вільні бібліотеки + + Елементи стеження переспрямуванням + + Очищує куки, встановлені переспрямовувачами на відомі вебсайти для стеження. + Підтримка diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index b8f1e02ef..7579c6547 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1158,6 +1158,8 @@ Đăng nhập bằng máy ảnh của bạn Sử dụng email thay thế + + Tạo một cái để đồng bộ hóa Firefox giữa các thiết bị.]]> 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. @@ -1265,6 +1267,11 @@ The first parameter is the app name --> %s | Thư viện OSS + + Trình theo dõi chuyển hướng + + Xóa cookie được đặt bởi chuyển hướng đến các trang web theo dõi đã biết. + Hỗ trợ diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index a953f900d..5bb4a79be 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1203,6 +1203,8 @@ 使用相机登录 改用电子邮件 + + 立即创建以在设备间同步 Firefox。]]> Firefox 将停止与您的账号的同步,但不会删除此设备上的任何浏览数据。 @@ -1311,6 +1313,11 @@ The first parameter is the app name --> %s | 开源软件库 + + 重定向跟踪器 + + 清除通过重定向设置到已知跟踪网站的 Cookie。 + 用户支持 @@ -1594,7 +1601,7 @@ 请启用标签页同步。 - 您其他设备上的 Firefox 中没有打开任何标签页。 + 您其他设备上的 Firefox 没有打开任何标签页。 查看您其他设备上的标签页列表。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 3c455dc1b..911a04098 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1192,6 +1192,8 @@ 使用攝影機登入 改用電子郵件 + + 註冊一組就能在不同裝置間同步 Firefox。]]> Firefox 將會停止與您帳號間的同步,但不會刪除此裝置上的任何瀏覽資料。 @@ -1299,6 +1301,11 @@ The first parameter is the app name --> %s | 開放原始碼程式庫 + + 重新導向追蹤器 + + 清除已知網站在重新導向時所設定的 Cookie。 + 技術支援 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 2401f4222..d7098c990 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -70,6 +70,12 @@ 8dp 8dp + 3dp + 8dp + + + 8dp + 56dp @@ -174,11 +180,8 @@ 69dp 4dp 11dp - 3dp 0dp - 8dp 0dp - 8dp 24dp 28dp diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 21e52240a..767ec44b0 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -60,6 +60,7 @@ pref_key_install_pwa_visits pref_key_times_app_opened pref_key_last_review_prompt_shown_time + pref_key_last_cfr_shown_time pref_key_telemetry @@ -212,6 +213,9 @@ pref_key_should_show_open_in_app_banner + + pref_key_should_show_auto_close_tabs_banner + pref_key_migrating_from_fenix_nightly_tip pref_key_migrating_from_firefox_nightly_tip pref_key_migrating_from_fenix_tip diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63c2472c8..39abc968d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1166,11 +1166,8 @@ Sign in with your camera Use email instead - - No account? %s to sync Firefox between devices. - - Create one + + Create one to sync Firefox between devices.]]> Firefox will stop syncing with your account, but won’t delete any of your browsing data on this device. diff --git a/app/src/migration/AndroidManifest.xml b/app/src/migration/AndroidManifest.xml index b89be3825..a90db330d 100644 --- a/app/src/migration/AndroidManifest.xml +++ b/app/src/migration/AndroidManifest.xml @@ -19,12 +19,10 @@ diff --git a/app/src/test/java/org/mozilla/fenix/addons/AddonsManagementViewTest.kt b/app/src/test/java/org/mozilla/fenix/addons/AddonsManagementViewTest.kt index 901910ed0..11ee765ce 100644 --- a/app/src/test/java/org/mozilla/fenix/addons/AddonsManagementViewTest.kt +++ b/app/src/test/java/org/mozilla/fenix/addons/AddonsManagementViewTest.kt @@ -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 { 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 { 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 { + 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 { + 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() diff --git a/app/src/test/java/org/mozilla/fenix/components/AccountAbnormalitiesTest.kt b/app/src/test/java/org/mozilla/fenix/components/AccountAbnormalitiesTest.kt index 21c0b404c..22a8df8ed 100644 --- a/app/src/test/java/org/mozilla/fenix/components/AccountAbnormalitiesTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/AccountAbnormalitiesTest.kt @@ -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(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()) diff --git a/app/src/test/java/org/mozilla/fenix/components/BackgroundServicesTest.kt b/app/src/test/java/org/mozilla/fenix/components/BackgroundServicesTest.kt index e0e93dad0..9104b8a98 100644 --- a/app/src/test/java/org/mozilla/fenix/components/BackgroundServicesTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/BackgroundServicesTest.kt @@ -78,10 +78,13 @@ class BackgroundServicesTest { fun `telemetry account observer tracks shared event`() { val account = mockk() - 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 diff --git a/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt b/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt index 124cf312a..268ac7e09 100644 --- a/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt @@ -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 ) diff --git a/app/src/test/java/org/mozilla/fenix/ext/DownloadItemKtTest.kt b/app/src/test/java/org/mozilla/fenix/ext/DownloadItemKtTest.kt index d4881dddb..e5fdf24b4 100644 --- a/app/src/test/java/org/mozilla/fenix/ext/DownloadItemKtTest.kt +++ b/app/src/test/java/org/mozilla/fenix/ext/DownloadItemKtTest.kt @@ -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()) } } diff --git a/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt b/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt index 6ad511775..4ae80cffa 100644 --- a/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt @@ -410,7 +410,7 @@ class DefaultSessionControlControllerTest { verify { navController.navigate( - match { it.actionId == R.id.action_global_search }, + match { it.actionId == R.id.action_global_search_dialog }, null ) } diff --git a/app/src/test/java/org/mozilla/fenix/home/intent/StartSearchIntentProcessorTest.kt b/app/src/test/java/org/mozilla/fenix/home/intent/StartSearchIntentProcessorTest.kt index 2f7908383..0bc87d407 100644 --- a/app/src/test/java/org/mozilla/fenix/home/intent/StartSearchIntentProcessorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/intent/StartSearchIntentProcessorTest.kt @@ -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 ), diff --git a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingAutomaticSignInViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingAutomaticSignInViewHolderTest.kt index c81dad81d..80978e3f0 100644 --- a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingAutomaticSignInViewHolderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingAutomaticSignInViewHolderTest.kt @@ -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 { + 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 { 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 { 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) diff --git a/app/src/test/java/org/mozilla/fenix/library/bookmarks/UtilsKtTest.kt b/app/src/test/java/org/mozilla/fenix/library/bookmarks/UtilsKtTest.kt new file mode 100644 index 000000000..42f185f32 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/library/bookmarks/UtilsKtTest.kt @@ -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(), 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(), 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") + ) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/search/DefaultSearchControllerTest.kt b/app/src/test/java/org/mozilla/fenix/search/DefaultSearchControllerTest.kt deleted file mode 100644 index 52b8dbbf0..000000000 --- a/app/src/test/java/org/mozilla/fenix/search/DefaultSearchControllerTest.kt +++ /dev/null @@ -1,346 +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 androidx.appcompat.app.AlertDialog -import androidx.navigation.NavController -import androidx.navigation.NavDirections -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.just -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.spyk -import io.mockk.unmockkObject -import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest -import mozilla.components.browser.search.SearchEngine -import mozilla.components.browser.session.Session -import mozilla.components.browser.session.SessionManager -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.mozilla.fenix.BrowserDirection -import org.mozilla.fenix.HomeActivity -import org.mozilla.fenix.R -import org.mozilla.fenix.components.metrics.Event -import org.mozilla.fenix.components.metrics.MetricController -import org.mozilla.fenix.components.metrics.MetricsUtils -import org.mozilla.fenix.settings.SupportUtils -import org.mozilla.fenix.utils.Settings - -typealias AlertDialogBuilder = AlertDialog.Builder - -@ExperimentalCoroutinesApi -class DefaultSearchControllerTest { - - @MockK(relaxed = true) private lateinit var activity: HomeActivity - @MockK(relaxed = true) private lateinit var store: SearchFragmentStore - @MockK(relaxed = true) private lateinit var navController: NavController - @MockK private lateinit var searchEngine: SearchEngine - @MockK(relaxed = true) private lateinit var metrics: MetricController - @MockK(relaxed = true) private lateinit var settings: Settings - @MockK private lateinit var sessionManager: SessionManager - @MockK(relaxed = true) private lateinit var clearToolbarFocus: () -> Unit - - private lateinit var controller: DefaultSearchController - - @Before - fun setUp() { - MockKAnnotations.init(this) - mockkObject(MetricsUtils) - - every { store.state.tabId } returns "test-tab-id" - every { store.state.searchEngineSource.searchEngine } returns searchEngine - every { sessionManager.select(any()) } just Runs - every { navController.currentDestination } returns mockk { - every { id } returns R.id.searchFragment - } - every { MetricsUtils.createSearchEvent(searchEngine, activity, any()) } returns null - controller = DefaultSearchController( - activity = activity, - sessionManager = sessionManager, - store = store, - navController = navController, - settings = settings, - metrics = metrics, - clearToolbarFocus = clearToolbarFocus - ) - } - - @After - fun teardown() { - unmockkObject(MetricsUtils) - } - - @Test - fun handleUrlCommitted() { - val url = "https://www.google.com/" - - controller.handleUrlCommitted(url) - - verify { - activity.openToBrowserAndLoad( - searchTermOrURL = url, - newTab = false, - from = BrowserDirection.FromSearch, - engine = searchEngine - ) - } - verify { metrics.track(Event.EnteredUrl(false)) } - } - - @Test - fun handleSearchCommitted() { - val searchTerm = "Firefox" - - controller.handleUrlCommitted(searchTerm) - - verify { - activity.openToBrowserAndLoad( - searchTermOrURL = searchTerm, - newTab = false, - from = BrowserDirection.FromSearch, - engine = searchEngine - ) - } - verify { settings.incrementActiveSearchCount() } - } - - @Test - fun handleCrashesUrlCommitted() { - val url = "about:crashes" - every { activity.packageName } returns "org.mozilla.fenix" - - controller.handleUrlCommitted(url) - - verify { - activity.startActivity(any()) - } - } - - @Test - fun handleAddonsUrlCommitted() { - val url = "about:addons" - val directions = SearchFragmentDirections.actionGlobalAddonsManagementFragment() - - controller.handleUrlCommitted(url) - - verify { navController.navigate(directions) } - } - - @Test - fun handleMozillaUrlCommitted() { - val url = "moz://a" - - controller.handleUrlCommitted(url) - - verify { - activity.openToBrowserAndLoad( - searchTermOrURL = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.MANIFESTO), - newTab = false, - from = BrowserDirection.FromSearch, - engine = searchEngine - ) - } - verify { metrics.track(Event.EnteredUrl(false)) } - } - - @Test - fun handleEditingCancelled() = runBlockingTest { - controller.handleEditingCancelled() - - verify { - clearToolbarFocus() - } - } - - @Test - fun handleTextChangedNonEmpty() { - val text = "fenix" - - controller.handleTextChanged(text) - - verify { store.dispatch(SearchFragmentAction.UpdateQuery(text)) } - } - - @Test - fun handleTextChangedEmpty() { - val text = "" - - controller.handleTextChanged(text) - - verify { store.dispatch(SearchFragmentAction.UpdateQuery(text)) } - } - - @Test - fun `show search shortcuts when setting enabled AND query empty`() { - val text = "" - every { settings.shouldShowSearchShortcuts } returns true - - controller.handleTextChanged(text) - - verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(true)) } - } - - @Test - fun `show search shortcuts when setting enabled AND query equals url`() { - val text = "mozilla.org" - every { store.state.url } returns "mozilla.org" - every { settings.shouldShowSearchShortcuts } returns true - - controller.handleTextChanged(text) - - verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(true)) } - } - - @Test - fun `do not show search shortcuts when setting enabled AND query non-empty`() { - val text = "mozilla" - - controller.handleTextChanged(text) - - verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) } - } - - @Test - fun `do not show search shortcuts when setting disabled AND query empty AND url not matching query`() { - every { settings.shouldShowSearchShortcuts } returns false - - val text = "" - - controller.handleTextChanged(text) - - verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) } - } - - @Test - fun `do not show search shortcuts when setting disabled AND query non-empty`() { - every { settings.shouldShowSearchShortcuts } returns false - - val text = "mozilla" - - controller.handleTextChanged(text) - - verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) } - } - - @Test - fun handleUrlTapped() { - val url = "https://www.google.com/" - - controller.handleUrlTapped(url) - - verify { - activity.openToBrowserAndLoad( - searchTermOrURL = url, - newTab = false, - from = BrowserDirection.FromSearch - ) - } - verify { metrics.track(Event.EnteredUrl(false)) } - } - - @Test - fun handleSearchTermsTapped() { - val searchTerms = "fenix" - - controller.handleSearchTermsTapped(searchTerms) - - verify { - activity.openToBrowserAndLoad( - searchTermOrURL = searchTerms, - newTab = false, - from = BrowserDirection.FromSearch, - engine = searchEngine, - forceSearch = true - ) - } - } - - @Test - fun handleSearchShortcutEngineSelected() { - val searchEngine: SearchEngine = mockk(relaxed = true) - - controller.handleSearchShortcutEngineSelected(searchEngine) - - verify { store.dispatch(SearchFragmentAction.SearchShortcutEngineSelected(searchEngine)) } - verify { metrics.track(Event.SearchShortcutSelected(searchEngine, false)) } - } - - @Test - fun handleClickSearchEngineSettings() { - val directions: NavDirections = - SearchFragmentDirections.actionGlobalSearchEngineFragment() - - controller.handleClickSearchEngineSettings() - - verify { navController.navigate(directions) } - } - - @Test - fun handleSearchShortcutsButtonClicked_alreadyOpen() { - every { store.state.showSearchShortcuts } returns true - - controller.handleSearchShortcutsButtonClicked() - - verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) } - } - - @Test - fun handleSearchShortcutsButtonClicked_notYetOpen() { - every { store.state.showSearchShortcuts } returns false - - controller.handleSearchShortcutsButtonClicked() - - verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(true)) } - } - - @Test - fun handleExistingSessionSelected() { - val session = mockk() - - controller.handleExistingSessionSelected(session) - - verify { sessionManager.select(session) } - verify { activity.openToBrowser(from = BrowserDirection.FromSearch) } - } - - @Test - fun handleExistingSessionSelected_tabId_nullSession() { - every { sessionManager.findSessionById("tab-id") } returns null - - controller.handleExistingSessionSelected("tab-id") - - verify(inverse = true) { sessionManager.select(any()) } - verify(inverse = true) { activity.openToBrowser(from = BrowserDirection.FromSearch) } - } - - @Test - fun handleExistingSessionSelected_tabId() { - val session = mockk() - every { sessionManager.findSessionById("tab-id") } returns session - - controller.handleExistingSessionSelected("tab-id") - - verify { sessionManager.select(any()) } - verify { activity.openToBrowser(from = BrowserDirection.FromSearch) } - } - - @Test - fun `show camera permissions needed dialog`() { - val dialogBuilder: AlertDialogBuilder = mockk(relaxed = true) - - val spyController = spyk(controller) - every { spyController.buildDialog() } returns dialogBuilder - - spyController.handleCameraPermissionsNeeded() - - verify { dialogBuilder.show() } - } -} diff --git a/app/src/test/java/org/mozilla/fenix/searchdialog/SearchDialogControllerTest.kt b/app/src/test/java/org/mozilla/fenix/search/SearchDialogControllerTest.kt similarity index 95% rename from app/src/test/java/org/mozilla/fenix/searchdialog/SearchDialogControllerTest.kt rename to app/src/test/java/org/mozilla/fenix/search/SearchDialogControllerTest.kt index d30fde735..bbfed3309 100644 --- a/app/src/test/java/org/mozilla/fenix/searchdialog/SearchDialogControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/search/SearchDialogControllerTest.kt @@ -2,8 +2,9 @@ * 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 androidx.appcompat.app.AlertDialog import androidx.navigation.NavController import androidx.navigation.NavDirections import io.mockk.MockKAnnotations @@ -30,8 +31,8 @@ import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricsUtils -import org.mozilla.fenix.search.AlertDialogBuilder -import org.mozilla.fenix.search.SearchFragmentAction +import org.mozilla.fenix.search.SearchDialogFragmentDirections.Companion.actionGlobalAddonsManagementFragment +import org.mozilla.fenix.search.SearchDialogFragmentDirections.Companion.actionGlobalSearchEngineFragment import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.utils.Settings @@ -140,7 +141,7 @@ class SearchDialogControllerTest { @Test fun handleAddonsUrlCommitted() { val url = "about:addons" - val directions = SearchDialogFragmentDirections.actionGlobalAddonsManagementFragment() + val directions = actionGlobalAddonsManagementFragment() controller.handleUrlCommitted(url) @@ -288,8 +289,7 @@ class SearchDialogControllerTest { @Test fun handleClickSearchEngineSettings() { - val directions: NavDirections = - SearchDialogFragmentDirections.actionGlobalSearchEngineFragment() + val directions: NavDirections = actionGlobalSearchEngineFragment() controller.handleClickSearchEngineSettings() @@ -347,7 +347,7 @@ class SearchDialogControllerTest { @Test fun `show camera permissions needed dialog`() { - val dialogBuilder: AlertDialogBuilder = mockk(relaxed = true) + val dialogBuilder: AlertDialog.Builder = mockk(relaxed = true) val spyController = spyk(controller) every { spyController.buildDialog() } returns dialogBuilder diff --git a/app/src/test/java/org/mozilla/fenix/search/SearchInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/search/SearchDialogInteractorTest.kt similarity index 91% rename from app/src/test/java/org/mozilla/fenix/search/SearchInteractorTest.kt rename to app/src/test/java/org/mozilla/fenix/search/SearchDialogInteractorTest.kt index 7a3af7ef0..7fc3761d1 100644 --- a/app/src/test/java/org/mozilla/fenix/search/SearchInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/search/SearchDialogInteractorTest.kt @@ -14,15 +14,15 @@ import org.junit.Before import org.junit.Test @ExperimentalCoroutinesApi -class SearchInteractorTest { +class SearchDialogInteractorTest { - lateinit var searchController: DefaultSearchController - lateinit var interactor: SearchInteractor + lateinit var searchController: SearchDialogController + lateinit var interactor: SearchDialogInteractor @Before fun setup() { searchController = mockk(relaxed = true) - interactor = SearchInteractor( + interactor = SearchDialogInteractor( searchController ) } @@ -47,7 +47,7 @@ class SearchInteractorTest { @Test fun onTextChanged() { - val interactor = SearchInteractor(searchController) + val interactor = SearchDialogInteractor(searchController) interactor.onTextChanged("test") diff --git a/app/src/test/java/org/mozilla/fenix/settings/account/DefaultSyncControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/account/DefaultSyncControllerTest.kt index 1d1424d01..1866d9e44 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/account/DefaultSyncControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/account/DefaultSyncControllerTest.kt @@ -4,6 +4,7 @@ package org.mozilla.fenix.settings.account +import androidx.appcompat.app.AlertDialog import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK @@ -13,7 +14,6 @@ import io.mockk.verify import org.junit.Before import org.junit.Test import org.mozilla.fenix.HomeActivity -import org.mozilla.fenix.search.AlertDialogBuilder class DefaultSyncControllerTest { @@ -28,7 +28,7 @@ class DefaultSyncControllerTest { @Test fun `show camera permissions needed dialog`() { - val dialogBuilder: AlertDialogBuilder = mockk(relaxed = true) + val dialogBuilder: AlertDialog.Builder = mockk(relaxed = true) val spyController = spyk(syncController) every { spyController.buildDialog() } returns dialogBuilder diff --git a/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt b/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt index 0e478c3ba..c484c82a2 100644 --- a/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt @@ -248,4 +248,14 @@ class DefaultTabTrayControllerTest { showChooseCollectionDialog(listOf(session)) } } + + @Test + fun handleSetUpAutoCloseTabsClicked() { + controller.handleSetUpAutoCloseTabsClicked() + val directions = TabTrayDialogFragmentDirections.actionGlobalCloseTabSettingsFragment() + + verify { + navController.navigate(directions) + } + } } diff --git a/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractorTest.kt index 7fbb8306c..1e1f027e1 100644 --- a/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractorTest.kt @@ -106,4 +106,10 @@ class TabTrayFragmentInteractorTest { interactor.onEnterMultiselect() verify { controller.handleEnterMultiselect() } } + + @Test + fun onSetUpAutoCloseTabsClicked() { + interactor.onSetUpAutoCloseTabsClicked() + verify { controller.handleSetUpAutoCloseTabsClicked() } + } } diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlayTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlayTest.kt index b408aa945..81050a3e4 100644 --- a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlayTest.kt +++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlayTest.kt @@ -44,7 +44,7 @@ class TrackingProtectionOverlayTest { @Test fun `no-op when loading`() { - every { settings.shouldShowTrackingProtectionOnboarding } returns true + every { settings.shouldShowTrackingProtectionCfr } returns true every { session.trackerBlockingEnabled } returns true every { session.trackersBlocked } returns listOf(mockk()) @@ -54,7 +54,7 @@ class TrackingProtectionOverlayTest { @Test fun `no-op when should not show onboarding`() { - every { settings.shouldShowTrackingProtectionOnboarding } returns false + every { settings.shouldShowTrackingProtectionCfr } returns false overlay.onLoadingStateChanged(session, loading = false) verify(exactly = 0) { settings.incrementTrackingProtectionOnboardingCount() } @@ -62,7 +62,7 @@ class TrackingProtectionOverlayTest { @Test fun `no-op when tracking protection disabled`() { - every { settings.shouldShowTrackingProtectionOnboarding } returns true + every { settings.shouldShowTrackingProtectionCfr } returns true every { session.trackerBlockingEnabled } returns false overlay.onLoadingStateChanged(session, loading = false) @@ -71,7 +71,7 @@ class TrackingProtectionOverlayTest { @Test fun `no-op when no trackers blocked`() { - every { settings.shouldShowTrackingProtectionOnboarding } returns true + every { settings.shouldShowTrackingProtectionCfr } returns true every { session.trackerBlockingEnabled } returns true every { session.trackersBlocked } returns emptyList() @@ -82,7 +82,7 @@ class TrackingProtectionOverlayTest { @Test fun `show onboarding when trackers are blocked`() { every { toolbar.hasWindowFocus() } returns true - every { settings.shouldShowTrackingProtectionOnboarding } returns true + every { settings.shouldShowTrackingProtectionCfr } returns true every { session.trackerBlockingEnabled } returns true every { session.trackersBlocked } returns listOf(mockk()) @@ -93,7 +93,7 @@ class TrackingProtectionOverlayTest { @Test fun `no-op when toolbar doesn't have focus`() { every { toolbar.hasWindowFocus() } returns false - every { settings.shouldShowTrackingProtectionOnboarding } returns true + every { settings.shouldShowTrackingProtectionCfr } returns true every { session.trackerBlockingEnabled } returns true every { session.trackersBlocked } returns listOf(mockk()) diff --git a/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt b/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt index 72b4fcafb..9b5861c99 100644 --- a/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt +++ b/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt @@ -117,6 +117,20 @@ class SettingsTest { assertFalse(settings.isRemoteDebuggingEnabled) } + @Test + fun canShowCfrTest() { + // When just created + // Then + assertEquals(0L, settings.lastCfrShownTimeInMillis) + assertTrue(settings.canShowCfr) + + // When + settings.lastCfrShownTimeInMillis = System.currentTimeMillis() + + // Then + assertFalse(settings.canShowCfr) + } + @Test fun isTelemetryEnabled() { // When just created @@ -394,25 +408,25 @@ class SettingsTest { fun showPwaFragment() { // When just created // Then - assertFalse(settings.shouldShowPwaOnboarding) + assertFalse(settings.shouldShowPwaCfr) // When visited once settings.incrementVisitedInstallableCount() // Then - assertFalse(settings.shouldShowPwaOnboarding) + assertFalse(settings.shouldShowPwaCfr) // When visited twice settings.incrementVisitedInstallableCount() // Then - assertFalse(settings.shouldShowPwaOnboarding) + assertFalse(settings.shouldShowPwaCfr) // When visited thrice settings.incrementVisitedInstallableCount() // Then - assertTrue(settings.shouldShowPwaOnboarding) + assertTrue(settings.shouldShowPwaCfr) } @Test diff --git a/buildSrc/src/main/java/AndroidComponents.kt b/buildSrc/src/main/java/AndroidComponents.kt index 36b4427cd..0d7401dd0 100644 --- a/buildSrc/src/main/java/AndroidComponents.kt +++ b/buildSrc/src/main/java/AndroidComponents.kt @@ -3,5 +3,5 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ object AndroidComponents { - const val VERSION = "60.0.20200921130100" + const val VERSION = "61.0.20200925190057" } diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 2a355dede..f2a55f5b4 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -15,7 +15,7 @@ object Versions { const val androidx_appcompat = "1.2.0-rc01" const val androidx_biometric = "1.1.0-alpha01" const val androidx_coordinator_layout = "1.1.0-rc01" - const val androidx_constraint_layout = "2.0.0-beta6" + const val androidx_constraint_layout = "2.0.0" const val androidx_preference = "1.1.0" const val androidx_legacy = "1.0.0" const val androidx_annotation = "1.1.0" diff --git a/docs/metrics.md b/docs/metrics.md index 197643258..845a399b9 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -1,7 +1,7 @@ # Metrics -This document enumerates the metrics collected by this project. +This document enumerates the metrics collected by this project using the [Glean SDK](https://mozilla.github.io/glean/book/index.html). This project may depend on other projects which also collect metrics. This means you might have to go searching through the dependency tree to get a full picture of everything collected by this project. diff --git a/l10n.toml b/l10n.toml index 643f4f0d1..cb946c589 100644 --- a/l10n.toml +++ b/l10n.toml @@ -75,6 +75,7 @@ locales = [ "rm", "ro", "ru", + "sat-Olck", "sk", "sl", "sq", @@ -83,6 +84,7 @@ locales = [ "sv-SE", "ta", "te", + "tg", "th", "tr", "trs", diff --git a/taskcluster/docker/ui-tests/Dockerfile b/taskcluster/docker/ui-tests/Dockerfile index 445ae433b..4a14d2ebf 100644 --- a/taskcluster/docker/ui-tests/Dockerfile +++ b/taskcluster/docker/ui-tests/Dockerfile @@ -11,7 +11,7 @@ USER worker:worker ENV GOOGLE_SDK_DOWNLOAD ./gcloud.tar.gz ENV GOOGLE_SDK_VERSION 233 -ENV FLANK_VERSION v20.08.0 +ENV FLANK_VERSION v20.09.3 ENV TEST_TOOLS /builds/worker/test-tools ENV PATH ${PATH}:${TEST_TOOLS}:${TEST_TOOLS}/google-cloud-sdk/bin