diff --git a/app/build.gradle b/app/build.gradle index 3a29bc509..893df8ab1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -422,6 +422,7 @@ dependencies { implementation Deps.mozilla_feature_site_permissions implementation Deps.mozilla_feature_readerview implementation Deps.mozilla_feature_tab_collections + implementation Deps.mozilla_feature_recentlyclosed implementation Deps.mozilla_feature_top_sites implementation Deps.mozilla_feature_share implementation Deps.mozilla_feature_accounts_push diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt index 4c6f93a8a..036273f24 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt @@ -11,6 +11,7 @@ import okhttp3.mockwebserver.MockWebServer import org.junit.Rule import org.junit.Before import org.junit.After +import org.junit.Ignore import org.junit.Test import org.mozilla.fenix.R import org.mozilla.fenix.helpers.AndroidAssetDispatcher @@ -88,6 +89,7 @@ class SettingsAddonsTest { } } + @Ignore("Failing intermittently on Firebase: https://github.com/mozilla-mobile/fenix/issues/13829") // Opens the addons settings menu, installs an addon, then uninstalls @Test fun verifyAddonsCanBeUninstalled() { diff --git a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt index 841305b36..84da2f843 100644 --- a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt +++ b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt @@ -32,5 +32,6 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) { FromAddonDetailsFragment(R.id.addonDetailsFragment), FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment), FromLoginDetailFragment(R.id.loginDetailFragment), - FromTabTray(R.id.tabTrayDialogFragment) + FromTabTray(R.id.tabTrayDialogFragment), + FromRecentlyClosed(R.id.recentlyClosedFragment) } diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index aaa99af95..d18c56462 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -83,6 +83,7 @@ import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor import org.mozilla.fenix.home.intent.StartSearchIntentProcessor import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections import org.mozilla.fenix.library.history.HistoryFragmentDirections +import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections import org.mozilla.fenix.perf.Performance import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.search.SearchFragmentDirections @@ -703,6 +704,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromTabTray -> TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId) + BrowserDirection.FromRecentlyClosed -> + RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId) } /** 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 63d82b9e3..c85545e95 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -771,20 +771,35 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session } private fun initializeEngineView(toolbarHeight: Int) { - engineView.setDynamicToolbarMaxHeight(toolbarHeight) - val context = requireContext() - val behavior = when (context.settings().toolbarPosition) { - ToolbarPosition.BOTTOM -> EngineViewBottomBehavior(context, null) - ToolbarPosition.TOP -> SwipeRefreshScrollingViewBehavior( - context, - null, - engineView, - browserToolbarView - ) - } - (swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior + if (context.settings().isDynamicToolbarEnabled) { + engineView.setDynamicToolbarMaxHeight(toolbarHeight) + + val behavior = when (context.settings().toolbarPosition) { + // Set engineView dynamic vertical clipping depending on the toolbar position. + ToolbarPosition.BOTTOM -> EngineViewBottomBehavior(context, null) + // Set scroll flags depending on if if the browser or the website is doing the scroll. + ToolbarPosition.TOP -> SwipeRefreshScrollingViewBehavior( + context, + null, + engineView, + browserToolbarView + ) + } + + (swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior + } else { + // Ensure webpage's bottom elements are aligned to the very bottom of the engineView. + engineView.setDynamicToolbarMaxHeight(0) + + // Effectively place the engineView on top of the toolbar if that is not dynamic. + if (context.settings().shouldUseBottomToolbar) { + val browserEngine = swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams + browserEngine.bottomMargin = + requireContext().resources.getDimensionPixelSize(R.dimen.browser_toolbar_height) + } + } } /** @@ -916,14 +931,13 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session */ protected open fun removeSessionIfNeeded(): Boolean { getSessionById()?.let { session -> - val sessionManager = requireComponents.core.sessionManager return if (session.source == SessionState.Source.ACTION_VIEW) { activity?.finish() - sessionManager.remove(session) + requireComponents.useCases.tabsUseCases.removeTab(session) true } else { if (session.hasParentSession) { - sessionManager.remove(session, true) + requireComponents.useCases.tabsUseCases.removeTab(session) } // We want to return to home if this session didn't have a parent session to select. val goToOverview = !session.hasParentSession @@ -1099,7 +1113,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session if (webAppToolbarShouldBeVisible) { browserToolbarView.view.isVisible = true val toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height) - engineView.setDynamicToolbarMaxHeight(toolbarHeight) + initializeEngineView(toolbarHeight) } } } 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 56964573b..11310a2fe 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -54,6 +54,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { private val windowFeature = ViewBoundFeatureWrapper() private var readerModeAvailable = false + private var openInAppOnboardingObserver: OpenInAppOnboardingObserver? = null override fun onCreateView( inflater: LayoutInflater, @@ -159,15 +160,16 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { } session?.register(toolbarSessionObserver, viewLifecycleOwner, autoPause = true) - if (settings.shouldShowOpenInAppBanner) { - session?.register( - OpenInAppOnboardingObserver( - context = context, - navController = findNavController(), - settings = settings, - appLinksUseCases = context.components.useCases.appLinksUseCases, - container = browserToolbarView.view.parent as ViewGroup - ), + if (settings.shouldShowOpenInAppBanner && session != null) { + openInAppOnboardingObserver = OpenInAppOnboardingObserver( + context = context, + navController = findNavController(), + settings = settings, + appLinksUseCases = context.components.useCases.appLinksUseCases, + container = browserToolbarView.view.parent as ViewGroup + ) + session.register( + openInAppOnboardingObserver!!, owner = this, autoPause = true ) @@ -188,6 +190,16 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { subscribeToTabCollections() } + override fun onStop() { + super.onStop() + // This observer initialized in onStart has a reference to fragment's view. + // Prevent it leaking the view after the latter onDestroyView. + if (openInAppOnboardingObserver != null) { + getSessionById()?.unregister(openInAppOnboardingObserver!!) + openInAppOnboardingObserver = null + } + } + private fun subscribeToTabCollections() { Observer> { requireComponents.core.tabCollectionStorage.cachedTabCollections = it diff --git a/app/src/main/java/org/mozilla/fenix/components/Components.kt b/app/src/main/java/org/mozilla/fenix/components/Components.kt index c5036195f..e8ae3b7dc 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -4,9 +4,9 @@ package org.mozilla.fenix.components +import android.app.Application import android.content.Context import android.content.Intent -import androidx.core.content.getSystemService import androidx.core.net.toUri import mozilla.components.feature.addons.AddonManager import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker @@ -120,7 +120,7 @@ class Components(private val context: Context) { val migrationStore by lazy { MigrationStore() } val performance by lazy { PerformanceComponent() } val push by lazy { Push(context, analytics.crashReporter) } - val wifiConnectionMonitor by lazy { WifiConnectionMonitor(context.getSystemService()!!) } + val wifiConnectionMonitor by lazy { WifiConnectionMonitor(context as Application) } val settings by lazy { Settings(context) } diff --git a/app/src/main/java/org/mozilla/fenix/components/Core.kt b/app/src/main/java/org/mozilla/fenix/components/Core.kt index 79fedd99f..fbec09a5d 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -20,6 +20,7 @@ import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.engine.EngineMiddleware import mozilla.components.browser.session.storage.SessionStorage +import mozilla.components.browser.state.action.RecentlyClosedAction import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.storage.sync.PlacesBookmarksStorage @@ -27,6 +28,7 @@ import mozilla.components.browser.storage.sync.PlacesHistoryStorage import mozilla.components.browser.storage.sync.RemoteTabsStorage import mozilla.components.browser.thumbnails.ThumbnailsMiddleware import mozilla.components.browser.thumbnails.storage.ThumbnailStorage +import mozilla.components.concept.base.crash.CrashReporting import mozilla.components.concept.engine.DefaultSettings import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.mediaquery.PreferredColorScheme @@ -39,6 +41,7 @@ import mozilla.components.feature.media.middleware.MediaMiddleware import mozilla.components.feature.pwa.ManifestStorage import mozilla.components.feature.pwa.WebAppShortcutManager import mozilla.components.feature.readerview.ReaderViewMiddleware +import mozilla.components.feature.recentlyclosed.RecentlyClosedMiddleware import mozilla.components.feature.session.HistoryDelegate import mozilla.components.feature.top.sites.DefaultTopSitesStorage import mozilla.components.feature.top.sites.PinnedSiteStorage @@ -51,7 +54,6 @@ import mozilla.components.service.digitalassetlinks.RelationChecker import mozilla.components.service.digitalassetlinks.local.StatementApi import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker import mozilla.components.service.sync.logins.SyncableLoginsStorage -import mozilla.components.support.base.crash.CrashReporting import mozilla.components.support.locale.LocaleManager import org.mozilla.fenix.AppRequestInterceptor import org.mozilla.fenix.Config @@ -140,12 +142,15 @@ class Core(private val context: Context, private val crashReporter: CrashReporti val store by lazy { BrowserStore( middleware = listOf( + RecentlyClosedMiddleware(context, RECENTLY_CLOSED_MAX, engine), MediaMiddleware(context, MediaService::class.java), DownloadMiddleware(context, DownloadService::class.java), ReaderViewMiddleware(), ThumbnailsMiddleware(thumbnailStorage) ) + EngineMiddleware.create(engine, ::findSessionById) - ) + ).also { + it.dispatch(RecentlyClosedAction.InitializeRecentlyClosedState) + } } private fun findSessionById(tabId: String): Session? { @@ -344,7 +349,7 @@ class Core(private val context: Context, private val crashReporter: CrashReporti fun getPreferredColorScheme(): PreferredColorScheme { val inDark = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == - Configuration.UI_MODE_NIGHT_YES + Configuration.UI_MODE_NIGHT_YES return when { context.settings().shouldUseDarkTheme -> PreferredColorScheme.Dark context.settings().shouldUseLightTheme -> PreferredColorScheme.Light @@ -357,5 +362,6 @@ class Core(private val context: Context, private val crashReporter: CrashReporti private const val KEY_STRENGTH = 256 private const val KEY_STORAGE_NAME = "core_prefs" private const val PASSWORDS_KEY = "passwords" + private const val RECENTLY_CLOSED_MAX = 5 } } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/BreadcrumbsRecorder.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/BreadcrumbsRecorder.kt index 14399697b..6db1182b3 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/BreadcrumbsRecorder.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/BreadcrumbsRecorder.kt @@ -10,8 +10,8 @@ import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import androidx.navigation.NavController import androidx.navigation.NavDestination +import mozilla.components.concept.base.crash.Breadcrumb import mozilla.components.lib.crash.CrashReporter -import mozilla.components.support.base.crash.Breadcrumb /** * Records breadcrumbs when the fragment changes. 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 86f32927c..5b70eae28 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 @@ -22,6 +22,7 @@ import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.sessionsOfType +import org.mozilla.fenix.ext.settings /** * An interface that handles the view manipulation of the BrowserToolbar, triggered by the Interactor @@ -156,7 +157,9 @@ class DefaultBrowserToolbarController( } override fun handleScroll(offset: Int) { - engineView.setVerticalClipping(offset) + if (activity.settings().isDynamicToolbarEnabled) { + engineView.setVerticalClipping(offset) + } } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt index 4f0673f2f..de7d4da8e 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt @@ -232,8 +232,12 @@ class BrowserToolbarView( fun setScrollFlags(shouldDisableScroll: Boolean = false) { when (settings.toolbarPosition) { ToolbarPosition.BOTTOM -> { - (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - behavior = BrowserToolbarBottomBehavior(view.context, null) + if (settings.isDynamicToolbarEnabled) { + (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply { + behavior = BrowserToolbarBottomBehavior(view.context, null) + } + } else { + expand() } } ToolbarPosition.TOP -> { diff --git a/app/src/main/java/org/mozilla/fenix/ext/Activity.kt b/app/src/main/java/org/mozilla/fenix/ext/Activity.kt index e65ad9f35..0ab6efec0 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/Activity.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/Activity.kt @@ -7,7 +7,7 @@ package org.mozilla.fenix.ext import android.app.Activity import android.view.View import android.view.WindowManager -import mozilla.components.support.base.crash.Breadcrumb +import mozilla.components.concept.base.crash.Breadcrumb /** * Attempts to call immersive mode using the View to hide the status bar and navigation buttons. diff --git a/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt b/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt index 39c3f7bb4..1ceb0c2e6 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt @@ -12,7 +12,7 @@ import androidx.navigation.NavDirections import androidx.navigation.NavOptions import androidx.navigation.fragment.NavHostFragment.findNavController import androidx.navigation.fragment.findNavController -import mozilla.components.support.base.crash.Breadcrumb +import mozilla.components.concept.base.crash.Breadcrumb import org.mozilla.fenix.NavHostActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.Components 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 5ddd53818..bc392ac14 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -477,7 +477,7 @@ class HomeFragment : Fragment() { .let { SessionManager.Snapshot(it, selectedIndex) } tabs.forEach { - sessionManager.remove(it) + requireComponents.useCases.tabsUseCases.removeTab(it) } val snackbarMessage = if (sessionCode == ALL_PRIVATE_TABS) { @@ -505,7 +505,7 @@ class HomeFragment : Fragment() { val isSelected = session.id == requireComponents.core.store.state.selectedTabId ?: false - sessionManager.remove(session) + requireComponents.useCases.tabsUseCases.removeTab(sessionId) val snackbarMessage = if (snapshot.session.private) { requireContext().getString(R.string.snackbar_private_tab_closed) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSiteItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSiteItemViewHolder.kt index af5bb1de5..ff7df347f 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSiteItemViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSiteItemViewHolder.kt @@ -16,6 +16,7 @@ import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite.Type.DEFAULT import mozilla.components.feature.top.sites.TopSite.Type.FRECENT +import mozilla.components.feature.top.sites.TopSite.Type.PINNED import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.loadIntoView @@ -54,8 +55,14 @@ class TopSiteItemViewHolder( } fun bind(topSite: TopSite) { - this.topSite = topSite top_site_title.text = topSite.title + + pin_indicator.visibility = if (topSite.type == PINNED) { + View.VISIBLE + } else { + View.GONE + } + when (topSite.url) { SupportUtils.POCKET_TRENDING_URL -> { favicon_image.setImageDrawable(getDrawable(itemView.context, R.drawable.ic_pocket)) @@ -64,6 +71,8 @@ class TopSiteItemViewHolder( itemView.context.components.core.icons.loadIntoView(favicon_image, topSite.url) } } + + this.topSite = topSite } private fun onTouchEvent( diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt index 234082b74..7a0b33b1a 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt @@ -27,9 +27,9 @@ enum class HistoryItemTimeGroup { } } -class HistoryAdapter( - private val historyInteractor: HistoryInteractor -) : PagedListAdapter(historyDiffCallback), SelectionHolder { +class HistoryAdapter(private val historyInteractor: HistoryInteractor) : + PagedListAdapter(historyDiffCallback), + SelectionHolder { private var mode: HistoryFragmentState.Mode = HistoryFragmentState.Mode.Normal override val selectedItems get() = mode.selectedItems diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt index 46a99952e..069afb1df 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt @@ -8,6 +8,7 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.res.Resources import androidx.navigation.NavController +import androidx.navigation.NavOptions import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import mozilla.components.concept.engine.prompt.ShareData @@ -15,6 +16,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.FenixSnackbar +@Suppress("TooManyFunctions") interface HistoryController { fun handleOpen(item: HistoryItem, mode: BrowsingMode? = null) fun handleSelect(item: HistoryItem) @@ -26,8 +28,10 @@ interface HistoryController { fun handleCopyUrl(item: HistoryItem) fun handleShare(item: HistoryItem) fun handleRequestSync() + fun handleEnterRecentlyClosed() } +@Suppress("TooManyFunctions") class DefaultHistoryController( private val store: HistoryFragmentStore, private val navController: NavController, @@ -101,4 +105,11 @@ class DefaultHistoryController( store.dispatch(HistoryFragmentAction.FinishSync) } } + + override fun handleEnterRecentlyClosed() { + navController.navigate( + HistoryFragmentDirections.actionGlobalRecentlyClosed(), + NavOptions.Builder().setPopUpTo(R.id.recentlyClosedFragment, true).build() + ) + } } 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 3b5f6d7d8..b34f22bb4 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 @@ -25,6 +25,7 @@ import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch +import mozilla.components.browser.state.action.RecentlyClosedAction import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.service.fxa.sync.SyncReason @@ -49,12 +50,15 @@ import org.mozilla.fenix.utils.allowUndo @SuppressWarnings("TooManyFunctions", "LargeClass") class HistoryFragment : LibraryPageFragment(), UserInteractionHandler { private lateinit var historyStore: HistoryFragmentStore - private lateinit var historyView: HistoryView private lateinit var historyInteractor: HistoryInteractor private lateinit var viewModel: HistoryViewModel private var undoScope: CoroutineScope? = null private var pendingHistoryDeletionJob: (suspend () -> Unit)? = null + private var _historyView: HistoryView? = null + protected val historyView: HistoryView + get() = _historyView!! + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -91,7 +95,10 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl historyInteractor = HistoryInteractor( historyController ) - historyView = HistoryView(view.historyLayout, historyInteractor) + _historyView = HistoryView( + view.historyLayout, + historyInteractor + ) return view } @@ -234,6 +241,11 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl return historyView.onBackPressed() } + override fun onDestroyView() { + super.onDestroyView() + _historyView = null + } + private fun openItem(item: HistoryItem, mode: BrowsingMode? = null) { requireComponents.analytics.metrics.track(Event.HistoryItemOpened) @@ -255,8 +267,9 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl } setPositiveButton(R.string.delete_browsing_data_prompt_allow) { dialog: DialogInterface, _ -> historyStore.dispatch(HistoryFragmentAction.EnterDeletionMode) - viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch(IO) { requireComponents.analytics.metrics.track(Event.HistoryAllItemsRemoved) + requireComponents.core.store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction) requireComponents.core.historyStorage.deleteEverything() launch(Main) { viewModel.invalidate() diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt index 4ddf147a7..2c8b9056e 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt @@ -61,4 +61,8 @@ class HistoryInteractor( override fun onRequestSync() { historyController.handleRequestSync() } + + override fun onRecentlyClosedClicked() { + historyController.handleEnterRecentlyClosed() + } } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryView.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryView.kt index 09be8a3d7..c309a4fb9 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryView.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryView.kt @@ -77,6 +77,11 @@ interface HistoryViewInteractor : SelectionInteractor { * Called when the user requests a sync of the history */ fun onRequestSync() + + /** + * Called when the user clicks on recently closed tab button. + */ + fun onRecentlyClosedClicked() } /** diff --git a/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt index f833507e9..03016716e 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt @@ -5,10 +5,12 @@ package org.mozilla.fenix.library.history.viewholders import android.view.View +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.history_list_item.view.* import kotlinx.android.synthetic.main.library_site_item.view.* import org.mozilla.fenix.R +import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.hideAndDisable import org.mozilla.fenix.ext.showAndEnable import org.mozilla.fenix.library.SelectionHolder @@ -38,6 +40,10 @@ class HistoryListItemViewHolder( historyInteractor.onDeleteSome(selected) } } + + itemView.recently_closed.setOnClickListener { + historyInteractor.onRecentlyClosedClicked() + } } fun bind( @@ -56,7 +62,7 @@ class HistoryListItemViewHolder( itemView.history_layout.titleView.text = item.title itemView.history_layout.urlView.text = item.url - toggleDeleteButton(showDeleteButton, mode === HistoryFragmentState.Mode.Normal) + toggleTopContent(showDeleteButton, mode === HistoryFragmentState.Mode.Normal) val headerText = timeGroup?.humanReadable(itemView.context) toggleHeader(headerText) @@ -86,11 +92,11 @@ class HistoryListItemViewHolder( } } - private fun toggleDeleteButton( - showDeleteButton: Boolean, + private fun toggleTopContent( + showTopContent: Boolean, isNormalMode: Boolean ) { - if (showDeleteButton) { + if (showTopContent) { itemView.delete_button.run { visibility = View.VISIBLE @@ -102,7 +108,16 @@ class HistoryListItemViewHolder( alpha = DELETE_BUTTON_DISABLED_ALPHA } } + val numRecentTabs = itemView.context.components.core.store.state.closedTabs.size + itemView.recently_closed_tabs_description.text = String.format( + itemView.context.getString( + if (numRecentTabs == 1) + R.string.recently_closed_tab else R.string.recently_closed_tabs + ), numRecentTabs + ) + itemView.recently_closed.isVisible = true } else { + itemView.recently_closed.visibility = View.GONE itemView.delete_button.visibility = View.GONE } } diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedAdapter.kt new file mode 100644 index 000000000..db6121c1e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedAdapter.kt @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.library.recentlyclosed + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import mozilla.components.browser.state.state.ClosedTab + +class RecentlyClosedAdapter( + private val interactor: RecentlyClosedFragmentInteractor +) : ListAdapter(DiffCallback) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): RecentlyClosedItemViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(RecentlyClosedItemViewHolder.LAYOUT_ID, parent, false) + return RecentlyClosedItemViewHolder(view, interactor) + } + + override fun onBindViewHolder(holder: RecentlyClosedItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + private object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ClosedTab, newItem: ClosedTab) = + oldItem.id == newItem.id || oldItem.title == newItem.title || oldItem.url == newItem.url + + override fun areContentsTheSame(oldItem: ClosedTab, newItem: ClosedTab) = + oldItem.id == newItem.id || oldItem.title == newItem.title || oldItem.url == newItem.url + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt new file mode 100644 index 000000000..0f71bf023 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.library.recentlyclosed + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.res.Resources +import androidx.navigation.NavController +import androidx.navigation.NavOptions +import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.state.action.RecentlyClosedAction +import mozilla.components.browser.state.state.ClosedTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.prompt.ShareData +import mozilla.components.feature.recentlyclosed.ext.restoreTab +import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.R +import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.components.FenixSnackbar + +interface RecentlyClosedController { + fun handleOpen(item: ClosedTab, mode: BrowsingMode? = null) + fun handleDeleteOne(tab: ClosedTab) + fun handleCopyUrl(item: ClosedTab) + fun handleShare(item: ClosedTab) + fun handleNavigateToHistory() + fun handleRestore(item: ClosedTab) +} + +class DefaultRecentlyClosedController( + private val navController: NavController, + private val store: BrowserStore, + private val sessionManager: SessionManager, + private val resources: Resources, + private val snackbar: FenixSnackbar, + private val clipboardManager: ClipboardManager, + private val activity: HomeActivity, + private val openToBrowser: (item: ClosedTab, mode: BrowsingMode?) -> Unit +) : RecentlyClosedController { + override fun handleOpen(item: ClosedTab, mode: BrowsingMode?) { + openToBrowser(item, mode) + } + + override fun handleDeleteOne(tab: ClosedTab) { + store.dispatch(RecentlyClosedAction.RemoveClosedTabAction(tab)) + } + + override fun handleNavigateToHistory() { + navController.navigate( + RecentlyClosedFragmentDirections.actionGlobalHistoryFragment(), + NavOptions.Builder().setPopUpTo(R.id.historyFragment, true).build() + ) + } + + override fun handleCopyUrl(item: ClosedTab) { + val urlClipData = ClipData.newPlainText(item.url, item.url) + clipboardManager.setPrimaryClip(urlClipData) + with(snackbar) { + setText(resources.getString(R.string.url_copied)) + show() + } + } + + override fun handleShare(item: ClosedTab) { + navController.navigate( + RecentlyClosedFragmentDirections.actionGlobalShareFragment( + data = arrayOf(ShareData(url = item.url, title = item.title)) + ) + ) + } + + override fun handleRestore(item: ClosedTab) { + item.restoreTab( + store, + sessionManager, + onTabRestored = { + activity.openToBrowser( + from = BrowserDirection.FromRecentlyClosed + ) + } + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt new file mode 100644 index 000000000..743e33c18 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.library.recentlyclosed + +import android.content.ClipboardManager +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import kotlinx.android.synthetic.main.fragment_recently_closed_tabs.view.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import mozilla.components.browser.state.state.ClosedTab +import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged +import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.R +import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.components.FenixSnackbar +import org.mozilla.fenix.components.StoreProvider +import org.mozilla.fenix.ext.getRootView +import org.mozilla.fenix.ext.requireComponents +import org.mozilla.fenix.ext.showToolbar +import org.mozilla.fenix.library.LibraryPageFragment + +@Suppress("TooManyFunctions") +class RecentlyClosedFragment : LibraryPageFragment() { + private lateinit var recentlyClosedFragmentStore: RecentlyClosedFragmentStore + private var _recentlyClosedFragmentView: RecentlyClosedFragmentView? = null + protected val recentlyClosedFragmentView: RecentlyClosedFragmentView + get() = _recentlyClosedFragmentView!! + + private lateinit var recentlyClosedInteractor: RecentlyClosedFragmentInteractor + + override fun onResume() { + super.onResume() + showToolbar(getString(R.string.library_recently_closed_tabs)) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.library_menu, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + R.id.close_history -> { + close() + true + } + else -> super.onOptionsItemSelected(item) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_recently_closed_tabs, container, false) + recentlyClosedFragmentStore = StoreProvider.get(this) { + RecentlyClosedFragmentStore( + RecentlyClosedFragmentState( + items = listOf() + ) + ) + } + recentlyClosedInteractor = RecentlyClosedFragmentInteractor( + recentlyClosedController = DefaultRecentlyClosedController( + navController = findNavController(), + store = requireComponents.core.store, + activity = activity as HomeActivity, + sessionManager = requireComponents.core.sessionManager, + resources = requireContext().resources, + snackbar = FenixSnackbar.make( + view = requireActivity().getRootView()!!, + isDisplayedWithBrowserToolbar = true + ), + clipboardManager = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager, + openToBrowser = ::openItem + ) + ) + _recentlyClosedFragmentView = RecentlyClosedFragmentView( + view.recentlyClosedLayout, + recentlyClosedInteractor + ) + return view + } + + override fun onDestroyView() { + super.onDestroyView() + _recentlyClosedFragmentView = null + } + + private fun openItem(tab: ClosedTab, mode: BrowsingMode? = null) { + mode?.let { (activity as HomeActivity).browsingModeManager.mode = it } + + (activity as HomeActivity).openToBrowserAndLoad( + searchTermOrURL = tab.url, + newTab = true, + from = BrowserDirection.FromRecentlyClosed + ) + } + + @ExperimentalCoroutinesApi + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + consumeFrom(recentlyClosedFragmentStore) { + recentlyClosedFragmentView.update(it.items) + } + + requireComponents.core.store.flowScoped(viewLifecycleOwner) { flow -> + flow.map { state -> state.closedTabs } + .ifChanged() + .collect { tabs -> + recentlyClosedFragmentStore.dispatch( + RecentlyClosedFragmentAction.Change(tabs) + ) + } + } + } + + override val selectedItems: Set = setOf() +} diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractor.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractor.kt new file mode 100644 index 000000000..b62b430b2 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractor.kt @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.library.recentlyclosed + +import mozilla.components.browser.state.state.ClosedTab +import org.mozilla.fenix.browser.browsingmode.BrowsingMode + +/** + * Interactor for the recently closed screen + * Provides implementations for the RecentlyClosedInteractor + */ +class RecentlyClosedFragmentInteractor( + private val recentlyClosedController: RecentlyClosedController +) : RecentlyClosedInteractor { + override fun restore(item: ClosedTab) { + recentlyClosedController.handleRestore(item) + } + + override fun onCopyPressed(item: ClosedTab) { + recentlyClosedController.handleCopyUrl(item) + } + + override fun onSharePressed(item: ClosedTab) { + recentlyClosedController.handleShare(item) + } + + override fun onOpenInNormalTab(item: ClosedTab) { + recentlyClosedController.handleOpen(item, BrowsingMode.Normal) + } + + override fun onOpenInPrivateTab(item: ClosedTab) { + recentlyClosedController.handleOpen(item, BrowsingMode.Private) + } + + override fun onDeleteOne(tab: ClosedTab) { + recentlyClosedController.handleDeleteOne(tab) + } + + override fun onNavigateToHistory() { + recentlyClosedController.handleNavigateToHistory() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentStore.kt new file mode 100644 index 000000000..cb75dabca --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentStore.kt @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.library.recentlyclosed + +import mozilla.components.browser.state.state.ClosedTab +import mozilla.components.lib.state.Action +import mozilla.components.lib.state.State +import mozilla.components.lib.state.Store + +/** + * The [Store] for holding the [RecentlyClosedFragmentState] and applying [RecentlyClosedFragmentAction]s. + */ +class RecentlyClosedFragmentStore(initialState: RecentlyClosedFragmentState) : + Store( + initialState, + ::recentlyClosedStateReducer + ) + +/** + * Actions to dispatch through the `RecentlyClosedFragmentStore` to modify + * `RecentlyClosedFragmentState` through the reducer. + */ +sealed class RecentlyClosedFragmentAction : Action { + data class Change(val list: List) : RecentlyClosedFragmentAction() +} + +/** + * The state for the Recently Closed Screen + * @property items List of recently closed tabs to display + */ +data class RecentlyClosedFragmentState(val items: List = emptyList()) : State + +/** + * The RecentlyClosedFragmentState Reducer. + */ +private fun recentlyClosedStateReducer( + state: RecentlyClosedFragmentState, + action: RecentlyClosedFragmentAction +): RecentlyClosedFragmentState { + return when (action) { + is RecentlyClosedFragmentAction.Change -> state.copy(items = action.list) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentView.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentView.kt new file mode 100644 index 000000000..0ec06e48e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentView.kt @@ -0,0 +1,110 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.library.recentlyclosed + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.component_recently_closed.* +import mozilla.components.browser.state.state.ClosedTab +import org.mozilla.fenix.R + +interface RecentlyClosedInteractor { + /** + * Called when an item is tapped to restore it. + * + * @param item the tapped item to restore. + */ + fun restore(item: ClosedTab) + + /** + * Called when the view more history option is tapped. + */ + fun onNavigateToHistory() + + /** + * Copies the URL of a recently closed tab item to the copy-paste buffer. + * + * @param item the recently closed tab item to copy the URL from + */ + fun onCopyPressed(item: ClosedTab) + + /** + * Opens the share sheet for a recently closed tab item. + * + * @param item the recently closed tab item to share + */ + fun onSharePressed(item: ClosedTab) + + /** + * Opens a recently closed tab item in a new tab. + * + * @param item the recently closed tab item to open in a new tab + */ + fun onOpenInNormalTab(item: ClosedTab) + + /** + * Opens a recently closed tab item in a private tab. + * + * @param item the recently closed tab item to open in a private tab + */ + fun onOpenInPrivateTab(item: ClosedTab) + + /** + * Deletes one recently closed tab item. + * + * @param item the recently closed tab item to delete. + */ + fun onDeleteOne(tab: ClosedTab) +} + +/** + * View that contains and configures the Recently Closed List + */ +class RecentlyClosedFragmentView( + container: ViewGroup, + private val interactor: RecentlyClosedFragmentInteractor +) : LayoutContainer { + + override val containerView: ConstraintLayout = LayoutInflater.from(container.context) + .inflate(R.layout.component_recently_closed, container, true) + .findViewById(R.id.recently_closed_wrapper) + + private val recentlyClosedAdapter: RecentlyClosedAdapter = RecentlyClosedAdapter(interactor) + + init { + recently_closed_list.apply { + layoutManager = LinearLayoutManager(containerView.context) + adapter = recentlyClosedAdapter + } + + view_more_history.apply { + titleView.text = + containerView.context.getString(R.string.recently_closed_show_full_history) + urlView.isVisible = false + overflowView.isVisible = false + iconView.background = null + iconView.setImageDrawable( + ContextCompat.getDrawable( + containerView.context, + R.drawable.ic_history + ) + ) + setOnClickListener { + interactor.onNavigateToHistory() + } + } + } + + fun update(items: List) { + recently_closed_empty_view.isVisible = items.isEmpty() + recently_closed_list.isVisible = items.isNotEmpty() + recentlyClosedAdapter.submitList(items) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedItemViewHolder.kt new file mode 100644 index 000000000..e60cc34ea --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedItemViewHolder.kt @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.library.recentlyclosed + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.history_list_item.view.* +import mozilla.components.browser.state.state.ClosedTab +import org.mozilla.fenix.R +import org.mozilla.fenix.library.history.HistoryItemMenu +import org.mozilla.fenix.utils.Do + +class RecentlyClosedItemViewHolder( + view: View, + private val recentlyClosedFragmentInteractor: RecentlyClosedFragmentInteractor +) : RecyclerView.ViewHolder(view) { + + private var item: ClosedTab? = null + + init { + setupMenu() + } + + fun bind( + item: ClosedTab + ) { + itemView.history_layout.titleView.text = + if (item.title.isNotEmpty()) item.title else item.url + itemView.history_layout.urlView.text = item.url + + if (this.item?.url != item.url) { + itemView.history_layout.loadFavicon(item.url) + } + + itemView.setOnClickListener { + recentlyClosedFragmentInteractor.restore(item) + } + + this.item = item + } + + private fun setupMenu() { + val historyMenu = HistoryItemMenu(itemView.context) { + val item = this.item ?: return@HistoryItemMenu + Do exhaustive when (it) { + HistoryItemMenu.Item.Copy -> recentlyClosedFragmentInteractor.onCopyPressed(item) + HistoryItemMenu.Item.Share -> recentlyClosedFragmentInteractor.onSharePressed(item) + HistoryItemMenu.Item.OpenInNewTab -> recentlyClosedFragmentInteractor.onOpenInNormalTab( + item + ) + HistoryItemMenu.Item.OpenInPrivateTab -> recentlyClosedFragmentInteractor.onOpenInPrivateTab( + item + ) + HistoryItemMenu.Item.Delete -> recentlyClosedFragmentInteractor.onDeleteOne( + item + ) + } + } + + itemView.history_layout.attachMenu(historyMenu.menuController) + } + + companion object { + const val LAYOUT_ID = R.layout.history_list_item + } +} diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchController.kt b/app/src/main/java/org/mozilla/fenix/search/SearchController.kt index 8e668c7ea..058bf715b 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchController.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchController.kt @@ -4,7 +4,13 @@ package org.mozilla.fenix.search +import android.content.DialogInterface import android.content.Intent +import android.net.Uri +import android.os.Build +import android.text.SpannableString +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AlertDialog import androidx.navigation.NavController import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.session.Session @@ -29,6 +35,7 @@ import org.mozilla.fenix.utils.Settings /** * An interface that handles the view manipulation of the Search, triggered by the Interactor */ +@Suppress("TooManyFunctions") interface SearchController { fun handleUrlCommitted(url: String) fun handleEditingCancelled() @@ -40,6 +47,7 @@ interface SearchController { fun handleExistingSessionSelected(session: Session) fun handleExistingSessionSelected(tabId: String) fun handleSearchShortcutsButtonClicked() + fun handleCameraPermissionsNeeded() } @Suppress("TooManyFunctions", "LongParameterList") @@ -194,4 +202,51 @@ class DefaultSearchController( handleExistingSessionSelected(session) } } + + /** + * Creates and shows an [AlertDialog] when camera permissions are needed. + * + * In versions above M, [AlertDialog.BUTTON_POSITIVE] takes the user to the app settings. This + * intent only exists in M and above. Below M, [AlertDialog.BUTTON_POSITIVE] routes to a SUMO + * help page to find the app settings. + * + * [AlertDialog.BUTTON_NEGATIVE] dismisses the dialog. + */ + override fun handleCameraPermissionsNeeded() { + val dialog = buildDialog() + dialog.show() + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun buildDialog(): AlertDialog.Builder { + return AlertDialog.Builder(activity).apply { + val spannableText = SpannableString( + activity.resources.getString(R.string.camera_permissions_needed_message) + ) + setMessage(spannableText) + setNegativeButton(R.string.camera_permissions_needed_negative_button_text) { + dialog: DialogInterface, _ -> + dialog.cancel() + } + setPositiveButton(R.string.camera_permissions_needed_positive_button_text) { + dialog: DialogInterface, _ -> + val intent: Intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + } else { + SupportUtils.createCustomTabIntent( + activity, + SupportUtils.getSumoURLForTopic( + activity, + SupportUtils.SumoTopic.QR_CAMERA_ACCESS + ) + ) + } + val uri = Uri.fromParts("package", activity.packageName, null) + intent.data = uri + dialog.cancel() + activity.startActivity(intent) + } + create() + } + } } diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt index 01877979e..0da68c01d 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt @@ -26,6 +26,7 @@ import androidx.core.widget.NestedScrollView import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import androidx.preference.PreferenceManager import kotlinx.android.synthetic.main.fragment_search.* import kotlinx.android.synthetic.main.fragment_search.view.* import kotlinx.android.synthetic.main.search_suggestions_hint.view.* @@ -50,6 +51,7 @@ import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.hideToolbar import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings @@ -219,6 +221,7 @@ class SearchFragment : Fragment(), UserInteractionHandler { setNegativeButton(R.string.qr_scanner_dialog_negative) { dialog: DialogInterface, _ -> requireComponents.analytics.metrics.track(Event.QRScannerNavigationDenied) dialog.cancel() + resetFocus() } setPositiveButton(R.string.qr_scanner_dialog_positive) { dialog: DialogInterface, _ -> requireComponents.analytics.metrics.track(Event.QRScannerNavigationAllowed) @@ -229,6 +232,7 @@ class SearchFragment : Fragment(), UserInteractionHandler { from = BrowserDirection.FromSearch ) dialog.dismiss() + resetFocus() } create() }.show() @@ -241,8 +245,19 @@ class SearchFragment : Fragment(), UserInteractionHandler { view.search_scan_button.setOnClickListener { toolbarView.view.clearFocus() - requireComponents.analytics.metrics.track(Event.QRScannerOpened) - qrFeature.get()?.scan(R.id.container) + + val cameraPermissionsDenied = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean( + getPreferenceKey(R.string.pref_key_camera_permissions), + false + ) + + if (cameraPermissionsDenied) { + searchInteractor.onCameraPermissionsNeeded() + } else { + requireComponents.analytics.metrics.track(Event.QRScannerOpened) + qrFeature.get()?.scan(R.id.container) + } } view.search_engines_shortcut_button.setOnClickListener { @@ -368,15 +383,19 @@ class SearchFragment : Fragment(), UserInteractionHandler { override fun onBackPressed(): Boolean { return when { qrFeature.onBackPressed() -> { - toolbarView.view.edit.focus() - view?.search_scan_button?.isChecked = false - toolbarView.view.requestFocus() + resetFocus() true } else -> false } } + private fun resetFocus() { + search_scan_button.isChecked = false + toolbarView.view.edit.focus() + toolbarView.view.requestFocus() + } + private fun updateSearchWithLabel(searchState: SearchFragmentState) { search_engine_shortcut.visibility = if (searchState.showSearchShortcuts) View.VISIBLE else View.GONE @@ -408,8 +427,16 @@ class SearchFragment : Fragment(), UserInteractionHandler { context?.let { context: Context -> if (context.isPermissionGranted(Manifest.permission.CAMERA)) { permissionDidUpdate = true + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putBoolean( + getPreferenceKey(R.string.pref_key_camera_permissions), false + ).apply() } else { - view?.search_scan_button?.isChecked = false + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putBoolean( + getPreferenceKey(R.string.pref_key_camera_permissions), true + ).apply() + resetFocus() } } } diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchInteractor.kt b/app/src/main/java/org/mozilla/fenix/search/SearchInteractor.kt index dc4080aa4..823945453 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchInteractor.kt @@ -13,6 +13,7 @@ import org.mozilla.fenix.search.toolbar.ToolbarInteractor * Interactor for the search screen * Provides implementations for the AwesomeBarView and ToolbarView */ +@Suppress("TooManyFunctions") class SearchInteractor( private val searchController: SearchController ) : AwesomeBarInteractor, ToolbarInteractor { @@ -56,4 +57,8 @@ class SearchInteractor( override fun onExistingSessionSelected(tabId: String) { searchController.handleExistingSessionSelected(tabId) } + + fun onCameraPermissionsNeeded() { + searchController.handleCameraPermissionsNeeded() + } } diff --git a/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt b/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt index 79f11acf1..037c08708 100644 --- a/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt +++ b/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt @@ -19,6 +19,7 @@ import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.content.res.resolveAttribute import mozilla.components.support.ktx.android.view.hideKeyboard import org.mozilla.fenix.R +import org.mozilla.fenix.ext.settings import org.mozilla.fenix.search.SearchFragmentState /** @@ -110,12 +111,15 @@ class ToolbarView( } val engineForSpeculativeConnects = if (!isPrivate) engine else null - ToolbarAutocompleteFeature( - view, - engineForSpeculativeConnects - ).apply { - addDomainProvider(ShippedDomainsProvider().also { it.initialize(view.context) }) - historyStorage?.also(::addHistoryStorageProvider) + + if (context.settings().shouldAutocompleteInAwesomebar) { + ToolbarAutocompleteFeature( + view, + engineForSpeculativeConnects + ).apply { + addDomainProvider(ShippedDomainsProvider().also { it.initialize(view.context) }) + historyStorage?.also(::addHistoryStorageProvider) + } } } diff --git a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogController.kt b/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogController.kt index c4e81c041..476a7d615 100644 --- a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogController.kt +++ b/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogController.kt @@ -4,7 +4,13 @@ package org.mozilla.fenix.searchdialog +import android.content.DialogInterface import android.content.Intent +import android.net.Uri +import android.os.Build +import android.text.SpannableString +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AlertDialog import androidx.navigation.NavController import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.session.Session @@ -33,6 +39,7 @@ class SearchDialogController( private val navController: NavController, private val settings: Settings, private val metrics: MetricController, + private val dismissDialog: () -> Unit, private val clearToolbarFocus: () -> Unit ) : SearchController { @@ -45,12 +52,15 @@ class SearchDialogController( activity.startActivity(Intent(activity, CrashListActivity::class.java)) } "about:addons" -> { - val directions = SearchDialogFragmentDirections.actionGlobalAddonsManagementFragment() + val directions = + SearchDialogFragmentDirections.actionGlobalAddonsManagementFragment() navController.navigateSafe(R.id.searchDialogFragment, directions) } "moz://a" -> openSearchOrUrl(SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.MANIFESTO)) else -> if (url.isNotBlank()) { openSearchOrUrl(url) + } else { + dismissDialog() } } } @@ -182,4 +192,50 @@ class SearchDialogController( handleExistingSessionSelected(session) } } + + /** + * Creates and shows an [AlertDialog] when camera permissions are needed. + * + * In versions above M, [AlertDialog.BUTTON_POSITIVE] takes the user to the app settings. This + * intent only exists in M and above. Below M, [AlertDialog.BUTTON_POSITIVE] routes to a SUMO + * help page to find the app settings. + * + * [AlertDialog.BUTTON_NEGATIVE] dismisses the dialog. + */ + override fun handleCameraPermissionsNeeded() { + val dialog = buildDialog() + dialog.show() + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun buildDialog(): AlertDialog.Builder { + return AlertDialog.Builder(activity).apply { + val spannableText = SpannableString( + activity.resources.getString(R.string.camera_permissions_needed_message) + ) + setMessage(spannableText) + setNegativeButton(R.string.camera_permissions_needed_negative_button_text) { _, _ -> + dismissDialog() + } + setPositiveButton(R.string.camera_permissions_needed_positive_button_text) { + dialog: DialogInterface, _ -> + val intent: Intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + } else { + SupportUtils.createCustomTabIntent( + activity, + SupportUtils.getSumoURLForTopic( + activity, + SupportUtils.SumoTopic.QR_CAMERA_ACCESS + ) + ) + } + val uri = Uri.fromParts("package", activity.packageName, null) + intent.data = uri + dialog.cancel() + activity.startActivity(intent) + } + create() + } + } } diff --git a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt index a88a5f149..ff56f62f2 100644 --- a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt @@ -4,6 +4,7 @@ package org.mozilla.fenix.searchdialog +import android.Manifest import android.app.Activity import android.app.Dialog import android.content.Context @@ -28,21 +29,20 @@ import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import androidx.preference.PreferenceManager import kotlinx.android.synthetic.main.fragment_search_dialog.* -import kotlinx.android.synthetic.main.fragment_search_dialog.fill_link_from_clipboard -import kotlinx.android.synthetic.main.fragment_search_dialog.pill_wrapper -import kotlinx.android.synthetic.main.fragment_search_dialog.qr_scan_button -import kotlinx.android.synthetic.main.fragment_search_dialog.toolbar import kotlinx.android.synthetic.main.fragment_search_dialog.view.* import kotlinx.android.synthetic.main.search_suggestions_hint.view.* import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.browser.toolbar.BrowserToolbar +import mozilla.components.concept.storage.HistoryStorage import mozilla.components.feature.qr.QrFeature import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.content.hasCamera +import mozilla.components.support.ktx.android.content.isPermissionGranted import mozilla.components.support.ktx.android.content.res.getSpanned import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.ui.autocomplete.InlineAutocompleteEditText @@ -54,6 +54,8 @@ import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.getPreferenceKey +import org.mozilla.fenix.ext.isKeyboardVisible import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.search.SearchFragmentAction @@ -81,12 +83,17 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { private val qrFeature = ViewBoundFeatureWrapper() private val speechIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) + private var keyboardVisible: Boolean = false + override fun onStart() { super.onStart() // https://github.com/mozilla-mobile/fenix/issues/14279 // To prevent GeckoView from resizing we're going to change the softInputMode to not adjust // the size of the window. requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) + if (keyboardVisible) { + toolbarView.view.edit.focus() + } } override fun onStop() { @@ -94,6 +101,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { // https://github.com/mozilla-mobile/fenix/issues/14279 // Let's reset back to the default behavior after we're done searching requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + keyboardVisible = toolbarView.view.isKeyboardVisible() } override fun onCreate(savedInstanceState: Bundle?) { @@ -117,6 +125,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { val args by navArgs() val view = inflater.inflate(R.layout.fragment_search_dialog, container, false) val activity = requireActivity() as HomeActivity + val isPrivate = activity.browsingModeManager.mode.isPrivate requireComponents.analytics.metrics.track(Event.InteractWithSearchURLArea) @@ -138,8 +147,9 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { navController = findNavController(), settings = requireContext().settings(), metrics = requireComponents.analytics.metrics, + dismissDialog = { dismissAllowingStateLoss() }, clearToolbarFocus = { - toolbarView.view.hideKeyboard() + toolbarView.view.hideKeyboardAndSave() toolbarView.view.clearFocus() } ) @@ -148,8 +158,8 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { toolbarView = ToolbarView( requireContext(), interactor, - null, - false, + historyStorageProvider(), + isPrivate, view.toolbar, requireComponents.core.engine ).also(::addSearchButton) @@ -164,7 +174,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { setShortcutsChangedListener(FenixSearchEngineProvider.PREF_FILE_SEARCH_ENGINES) view.awesome_bar.setOnTouchListener { _, _ -> - view.hideKeyboard() + view.hideKeyboardAndSave() false } @@ -174,7 +184,6 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { .findViewById(R.id.mozac_browser_toolbar_edit_url_view) urlView?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO - val isPrivate = (requireActivity() as HomeActivity).browsingModeManager.mode.isPrivate requireComponents.core.engine.speculativeCreateSession(isPrivate) return view @@ -188,7 +197,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { setupConstraints(view) search_wrapper.setOnClickListener { - it.hideKeyboard() + it.hideKeyboardAndSave() dismissAllowingStateLoss() } @@ -202,8 +211,22 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { if (!requireContext().hasCamera()) { return@setOnClickListener } toolbarView.view.clearFocus() - requireComponents.analytics.metrics.track(Event.QRScannerOpened) - qrFeature.get()?.scan(R.id.search_wrapper) + + val cameraPermissionsDenied = + PreferenceManager.getDefaultSharedPreferences(context).getBoolean( + getPreferenceKey(R.string.pref_key_camera_permissions), + false + ) + + if (cameraPermissionsDenied) { + interactor.onCameraPermissionsNeeded() + resetFocus() + view.hideKeyboard() + toolbarView.view.requestFocus() + } else { + requireComponents.analytics.metrics.track(Event.QRScannerOpened) + qrFeature.get()?.scan(R.id.search_wrapper) + } } fill_link_from_clipboard.setOnClickListener { @@ -278,6 +301,19 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { } } + override fun onResume() { + super.onResume() + resetFocus() + toolbarView.view.edit.focus() + } + + override fun onPause() { + super.onPause() + qr_scan_button.isChecked = false + view?.hideKeyboard() + toolbarView.view.requestFocus() + } + override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { if (requestCode == VoiceSearchActivity.SPEECH_REQUEST_CODE && resultCode == Activity.RESULT_OK) { intent?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.first()?.also { @@ -291,19 +327,23 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { override fun onBackPressed(): Boolean { return when { qrFeature.onBackPressed() -> { - toolbarView.view.edit.focus() - view?.qr_scan_button?.isChecked = false - toolbarView.view.requestFocus() + resetFocus() true } else -> { - view?.hideKeyboard() + view?.hideKeyboardAndSave() dismissAllowingStateLoss() true } } } + private fun historyStorageProvider(): HistoryStorage? { + return if (requireContext().settings().shouldShowHistorySuggestions) { + requireComponents.core.historyStorage + } else null + } + private fun createQrFeature(): QrFeature { return QrFeature( requireContext(), @@ -342,6 +382,39 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { }) } + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + when (requestCode) { + REQUEST_CODE_CAMERA_PERMISSIONS -> qrFeature.withFeature { + context?.let { context: Context -> + it.onPermissionsResult(permissions, grantResults) + if (!context.isPermissionGranted(Manifest.permission.CAMERA)) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putBoolean( + getPreferenceKey(R.string.pref_key_camera_permissions), true + ).apply() + resetFocus() + } else { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putBoolean( + getPreferenceKey(R.string.pref_key_camera_permissions), false + ).apply() + } + } + } + else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + } + + private fun resetFocus() { + qr_scan_button.isChecked = false + toolbarView.view.edit.focus() + toolbarView.view.requestFocus() + } + private fun setupConstraints(view: View) { if (view.context.settings().toolbarPosition == ToolbarPosition.BOTTOM) { ConstraintSet().apply { @@ -350,11 +423,12 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { clear(toolbar.id, TOP) connect(toolbar.id, BOTTOM, PARENT_ID, BOTTOM) - clear(awesome_bar.id, TOP) clear(pill_wrapper.id, BOTTOM) - connect(awesome_bar.id, TOP, PARENT_ID, TOP) connect(pill_wrapper.id, BOTTOM, toolbar.id, TOP) + clear(search_suggestions_hint.id, TOP) + connect(search_suggestions_hint.id, TOP, PARENT_ID, TOP) + clear(fill_link_from_clipboard.id, TOP) connect(fill_link_from_clipboard.id, BOTTOM, pill_wrapper.id, TOP) @@ -386,6 +460,15 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { ) } + /** + * Used to save keyboard status on stop/sleep, to be restored later. + * See #14559 + * */ + private fun View.hideKeyboardAndSave() { + keyboardVisible = false + this.hideKeyboard() + } + private fun launchVoiceSearch() { // Note if a user disables speech while the app is on the search fragment // the voice button will still be available and *will* cause a crash if tapped, diff --git a/app/src/main/java/org/mozilla/fenix/settings/PairFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/PairFragment.kt index b1127f2ce..224efa237 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/PairFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/PairFragment.kt @@ -4,21 +4,28 @@ package org.mozilla.fenix.settings +import android.content.DialogInterface +import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.os.VibrationEffect import android.os.Vibrator +import android.provider.Settings +import android.text.SpannableString import android.view.View +import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.fragment.app.Fragment import androidx.navigation.fragment.NavHostFragment.findNavController import androidx.navigation.fragment.findNavController +import androidx.preference.PreferenceManager import mozilla.components.feature.qr.QrFeature import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import org.mozilla.fenix.R +import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.showToolbar @@ -63,8 +70,18 @@ class PairFragment : Fragment(R.layout.fragment_pair), UserInteractionHandler { view = view ) + val cameraPermissionsDenied = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean( + getPreferenceKey(R.string.pref_key_camera_permissions), + false + ) + qrFeature.withFeature { - it.scan(R.id.pair_layout) + if (cameraPermissionsDenied) { + showPermissionsNeededDialog() + } else { + it.scan(R.id.pair_layout) + } } } @@ -99,10 +116,57 @@ class PairFragment : Fragment(R.layout.fragment_pair), UserInteractionHandler { qrFeature.withFeature { it.onPermissionsResult(permissions, grantResults) } + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putBoolean( + getPreferenceKey(R.string.pref_key_camera_permissions), false + ).apply() } else { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putBoolean( + getPreferenceKey(R.string.pref_key_camera_permissions), true + ).apply() findNavController().popBackStack(R.id.turnOnSyncFragment, false) } } } } + + /** + * Shows an [AlertDialog] when camera permissions are needed. + * + * In versions above M, [AlertDialog.BUTTON_POSITIVE] takes the user to the app settings. This + * intent only exists in M and above. Below M, [AlertDialog.BUTTON_POSITIVE] routes to a SUMO + * help page to find the app settings. + * + * [AlertDialog.BUTTON_NEGATIVE] dismisses the dialog. + */ + private fun showPermissionsNeededDialog() { + AlertDialog.Builder(requireContext()).apply { + val spannableText = SpannableString( + resources.getString(R.string.camera_permissions_needed_message) + ) + setMessage(spannableText) + setNegativeButton(R.string.camera_permissions_needed_negative_button_text) { + dialog: DialogInterface, _ -> + dialog.cancel() + } + setPositiveButton(R.string.camera_permissions_needed_positive_button_text) { + dialog: DialogInterface, _ -> + val intent: Intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + } else { + SupportUtils.createCustomTabIntent( + requireContext(), + SupportUtils.getSumoURLForTopic( + requireContext(), + SupportUtils.SumoTopic.QR_CAMERA_ACCESS + ) + ) + } + dialog.cancel() + startActivity(intent) + } + create() + }.show() + } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt index 5f45ac337..4bbf6c4f3 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt @@ -40,7 +40,8 @@ object SupportUtils { SEARCH_SUGGESTION("how-search-firefox-preview"), CUSTOM_SEARCH_ENGINES("custom-search-engines"), UPGRADE_FAQ("firefox-preview-upgrade-faqs"), - SYNC_SETUP("how-set-firefox-sync-firefox-preview") + SYNC_SETUP("how-set-firefox-sync-firefox-preview"), + QR_CAMERA_ACCESS("qr-camera-access") } enum class MozillaPage(internal val path: String) { diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt index 6f87de29d..8550cd650 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt @@ -26,6 +26,7 @@ fun deleteAndQuit(activity: Activity, coroutineScope: CoroutineScope, snackbar: activity.components.useCases.tabsUseCases.removeAllTabs, activity.components.core.historyStorage, activity.components.core.permissionStorage, + activity.components.core.store, activity.components.core.icons, activity.components.core.engine, coroutineContext diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt index 575e8e969..7b02cd526 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt @@ -7,6 +7,8 @@ package org.mozilla.fenix.settings.deletebrowsingdata import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import mozilla.components.browser.icons.BrowserIcons +import mozilla.components.browser.state.action.RecentlyClosedAction +import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.Engine import mozilla.components.concept.storage.HistoryStorage import mozilla.components.feature.tabs.TabsUseCases @@ -25,6 +27,7 @@ class DefaultDeleteBrowsingDataController( private val removeAllTabs: TabsUseCases.RemoveAllTabsUseCase, private val historyStorage: HistoryStorage, private val permissionStorage: PermissionStorage, + private val store: BrowserStore, private val iconsStorage: BrowserIcons, private val engine: Engine, private val coroutineContext: CoroutineContext = Dispatchers.Main @@ -41,6 +44,7 @@ class DefaultDeleteBrowsingDataController( engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES)) historyStorage.deleteEverything() iconsStorage.clear() + store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction) } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt index 733ae3ba2..77f867494 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt @@ -28,7 +28,6 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.metrics.Event -import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar @@ -45,11 +44,12 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da super.onViewCreated(view, savedInstanceState) controller = DefaultDeleteBrowsingDataController( - requireContext().components.useCases.tabsUseCases.removeAllTabs, - requireContext().components.core.historyStorage, - requireContext().components.core.permissionStorage, - requireContext().components.core.icons, - requireContext().components.core.engine + requireComponents.useCases.tabsUseCases.removeAllTabs, + requireComponents.core.historyStorage, + requireComponents.core.permissionStorage, + requireComponents.core.store, + requireComponents.core.icons, + requireComponents.core.engine ) settings = requireContext().settings() diff --git a/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt index 800bf05bc..1c69aba76 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt @@ -32,6 +32,11 @@ class SearchEngineFragment : PreferenceFragmentCompat() { isChecked = context.settings().shouldShowSearchSuggestions } + val autocompleteURLsPreference = + requirePreference(R.string.pref_key_enable_autocomplete_urls).apply { + isChecked = context.settings().shouldAutocompleteInAwesomebar + } + val searchSuggestionsInPrivatePreference = requirePreference(R.string.pref_key_show_search_suggestions_in_private).apply { isChecked = context.settings().shouldShowSearchSuggestionsInPrivate @@ -73,6 +78,7 @@ class SearchEngineFragment : PreferenceFragmentCompat() { showClipboardSuggestions.onPreferenceChangeListener = SharedPreferenceUpdater() searchSuggestionsInPrivatePreference.onPreferenceChangeListener = SharedPreferenceUpdater() showVoiceSearchPreference.onPreferenceChangeListener = SharedPreferenceUpdater() + autocompleteURLsPreference.onPreferenceChangeListener = SharedPreferenceUpdater() searchSuggestionsPreference.setOnPreferenceClickListener { if (!searchSuggestionsPreference.isChecked) { 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 c3b4df91a..431bfe898 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt @@ -9,8 +9,7 @@ import androidx.navigation.NavController import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager -import mozilla.components.browser.storage.sync.Tab as SyncTab -import mozilla.components.concept.engine.profiler.Profiler +import mozilla.components.concept.base.profiler.Profiler import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.tabstray.Tab import mozilla.components.feature.tabs.TabsUseCases @@ -21,6 +20,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.home.HomeFragment +import mozilla.components.browser.storage.sync.Tab as SyncTab /** * [TabTrayDialogFragment] controller. @@ -42,6 +42,7 @@ interface TabTrayController { fun handleRemoveSelectedTab(tab: Tab) fun handleOpenTab(tab: Tab) fun handleEnterMultiselect() + fun handleRecentlyClosedClicked() } /** @@ -178,4 +179,9 @@ class DefaultTabTrayController( override fun handleEnterMultiselect() { tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.EnterMultiSelectMode) } + + override fun handleRecentlyClosedClicked() { + val directions = TabTrayDialogFragmentDirections.actionGlobalRecentlyClosed() + navController.navigate(directions) + } } 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 2ec18d928..3cf89a386 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt @@ -73,6 +73,11 @@ interface TabTrayInteractor { * Called when multiselect mode should be entered with no tabs selected. */ fun onEnterMultiselect() + + /** + * Called when user clicks the recently closed tabs menu button. + */ + fun onOpenRecentlyClosedClicked() } /** @@ -92,6 +97,10 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab controller.handleTabSettingsClicked() } + override fun onOpenRecentlyClosedClicked() { + controller.handleRecentlyClosedClicked() + } + override fun onShareTabsClicked(private: Boolean) { controller.onShareTabsClicked(private) } 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 ba4e167eb..ef1ef8a98 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt @@ -302,6 +302,7 @@ class TabTrayView( is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked( isPrivateModeSelected ) + is TabTrayItemMenu.Item.OpenRecentlyClosed -> interactor.onOpenRecentlyClosedClicked() } } @@ -747,6 +748,7 @@ class TabTrayItemMenu( object OpenTabSettings : Item() object SaveToCollection : Item() object CloseAllTabs : Item() + object OpenRecentlyClosed : Item() } val menuBuilder by lazy { BrowserMenuBuilder(menuItems) } @@ -776,6 +778,13 @@ class TabTrayItemMenu( onItemTapped.invoke(Item.OpenTabSettings) }, + SimpleBrowserMenuItem( + context.getString(R.string.tab_tray_menu_recently_closed), + textColorResource = R.color.primary_text_normal_theme + ) { + onItemTapped.invoke(Item.OpenRecentlyClosed) + }, + SimpleBrowserMenuItem( context.getString(R.string.tab_tray_menu_item_close), textColorResource = R.color.primary_text_normal_theme 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 78ee5c67c..b059c5485 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -621,6 +621,11 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = true ) + val shouldAutocompleteInAwesomebar by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_enable_autocomplete_urls), + default = true + ) + var defaultTopSitesAdded by booleanPreference( appContext.getPreferenceKey(R.string.default_top_sites_added), default = false diff --git a/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt b/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt index cee0a6a03..eec7d25e6 100644 --- a/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt @@ -18,21 +18,29 @@ import org.mozilla.fenix.utils.Settings class SitePermissionsWifiIntegration( private val settings: Settings, private val wifiConnectionMonitor: WifiConnectionMonitor -) : LifecycleAwareFeature, WifiConnectionMonitor.Observer { +) : LifecycleAwareFeature { /** * Adds listener for autoplay setting [AUTOPLAY_ALLOW_ON_WIFI]. Sets all autoplay to allowed when * WIFI is connected, blocked otherwise. */ - override fun onWifiConnectionChanged(connected: Boolean) { - val setting = - if (connected) SitePermissionsRules.Action.ALLOWED else SitePermissionsRules.Action.BLOCKED - if (settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) == AUTOPLAY_ALLOW_ON_WIFI) { - settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.AUTOPLAY_AUDIBLE, setting) - settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.AUTOPLAY_INAUDIBLE, setting) - } else { - // The autoplay setting has changed, we can remove the listener - removeWifiConnectedListener() + private val wifiConnectedListener: ((Boolean) -> Unit) by lazy { + { connected: Boolean -> + val setting = + if (connected) SitePermissionsRules.Action.ALLOWED else SitePermissionsRules.Action.BLOCKED + if (settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) == AUTOPLAY_ALLOW_ON_WIFI) { + settings.setSitePermissionsPhoneFeatureAction( + PhoneFeature.AUTOPLAY_AUDIBLE, + setting + ) + settings.setSitePermissionsPhoneFeatureAction( + PhoneFeature.AUTOPLAY_INAUDIBLE, + setting + ) + } else { + // The autoplay setting has changed, we can remove the listener + removeWifiConnectedListener() + } } } @@ -47,11 +55,11 @@ class SitePermissionsWifiIntegration( } fun addWifiConnectedListener() { - wifiConnectionMonitor.register(this) + wifiConnectionMonitor.addOnWifiConnectedChangedListener(wifiConnectedListener) } fun removeWifiConnectedListener() { - wifiConnectionMonitor.unregister(this) + wifiConnectionMonitor.removeOnWifiConnectedChangedListener(wifiConnectedListener) } // Until https://bugzilla.mozilla.org/show_bug.cgi?id=1621825 is fixed, AUTOPLAY_ALLOW_ALL diff --git a/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt b/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt index b8e0c2ee9..34a6e5f7a 100644 --- a/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt +++ b/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt @@ -5,12 +5,11 @@ package org.mozilla.fenix.wifi import android.app.Application +import android.content.Context import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest -import mozilla.components.support.base.observer.Observable -import mozilla.components.support.base.observer.ObserverRegistry /** * Attaches itself to the [Application] and listens for WIFI available/not available events. This @@ -26,28 +25,30 @@ import mozilla.components.support.base.observer.ObserverRegistry * app.components.wifiConnectionListener.start() * ``` */ -class WifiConnectionMonitor( - private val connectivityManager: ConnectivityManager -) : Observable by ObserverRegistry() { +class WifiConnectionMonitor(app: Application) { + private val callbacks = mutableSetOf<(Boolean) -> Unit>() + private val connectivityManager = app.getSystemService(Context.CONNECTIVITY_SERVICE) as + ConnectivityManager - private var callbackReceived: Boolean = false + private var lastKnownStateWasAvailable: Boolean? = null private var isRegistered = false private val frameworkListener = object : ConnectivityManager.NetworkCallback() { override fun onLost(network: Network?) { - notifyAtLeastOneObserver { onWifiConnectionChanged(connected = false) } - callbackReceived = true + callbacks.forEach { it(false) } + lastKnownStateWasAvailable = false } override fun onAvailable(network: Network?) { - notifyAtLeastOneObserver { onWifiConnectionChanged(connected = true) } - callbackReceived = true + callbacks.forEach { it(true) } + lastKnownStateWasAvailable = true } } /** * Attaches the [WifiConnectionMonitor] to the application. After this has been called, callbacks - * added via [register] will be called until either the app exits, or [stop] is called. + * added via [addOnWifiConnectedChangedListener] will be called until either the app exits, or + * [stop] is called. * * Any existing callbacks will be called with the current state when this is called. */ @@ -61,8 +62,10 @@ class WifiConnectionMonitor( // AFAICT, the framework does not send an event when a new NetworkCallback is registered // while the WIFI is not connected, so we push this manually. If the WIFI is on, it will send // a follow up event shortly - if (!callbackReceived) { - notifyAtLeastOneObserver { onWifiConnectionChanged(connected = false) } + val noCallbacksReceivedYet = lastKnownStateWasAvailable == null + if (noCallbacksReceivedYet) { + lastKnownStateWasAvailable = false + callbacks.forEach { it(false) } } connectivityManager.registerNetworkCallback(request, frameworkListener) @@ -71,7 +74,7 @@ class WifiConnectionMonitor( /** * Detatches the [WifiConnectionMonitor] from the app. No callbacks added via - * [register] will be called after this has been called. + * [addOnWifiConnectedChangedListener] will be called after this has been called. */ fun stop() { // Framework code will throw if an unregistered listener attempts to unregister. @@ -80,7 +83,25 @@ class WifiConnectionMonitor( isRegistered = false } - interface Observer { - fun onWifiConnectionChanged(connected: Boolean) + /** + * Adds [onWifiChanged] to a list of listeners that will be called whenever WIFI connects or + * disconnects. + * + * If [onWifiChanged] is successfully added (i.e., it is a new listener), it will be immediately + * called with the last known state. + */ + fun addOnWifiConnectedChangedListener(onWifiChanged: (Boolean) -> Unit) { + val lastKnownState = lastKnownStateWasAvailable + if (callbacks.add(onWifiChanged) && lastKnownState != null) { + onWifiChanged(lastKnownState) + } + } + + /** + * Removes [onWifiChanged] from the list of listeners to be called whenever WIFI connects or + * disconnects. + */ + fun removeOnWifiConnectedChangedListener(onWifiChanged: (Boolean) -> Unit) { + callbacks.remove(onWifiChanged) } } diff --git a/app/src/main/res/drawable/ic_pin.xml b/app/src/main/res/drawable/ic_pin.xml new file mode 100644 index 000000000..6d28ef432 --- /dev/null +++ b/app/src/main/res/drawable/ic_pin.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/layout/component_recently_closed.xml b/app/src/main/res/layout/component_recently_closed.xml new file mode 100644 index 000000000..e0b70db2c --- /dev/null +++ b/app/src/main/res/layout/component_recently_closed.xml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/component_tabstray_bottom.xml b/app/src/main/res/layout/component_tabstray_bottom.xml index 59c777c05..f03df49b0 100644 --- a/app/src/main/res/layout/component_tabstray_bottom.xml +++ b/app/src/main/res/layout/component_tabstray_bottom.xml @@ -138,6 +138,7 @@ app:layout_constraintBottom_toBottomOf="@id/tab_layout" app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow" app:layout_constraintTop_toTopOf="@id/tab_layout" + app:tint="@color/primary_text_normal_theme" app:srcCompat="@drawable/ic_new" /> + + diff --git a/app/src/main/res/layout/fragment_search_dialog.xml b/app/src/main/res/layout/fragment_search_dialog.xml index ddef5fdec..fec3110b8 100644 --- a/app/src/main/res/layout/fragment_search_dialog.xml +++ b/app/src/main/res/layout/fragment_search_dialog.xml @@ -42,8 +42,7 @@ app:layout_constraintBottom_toTopOf="@+id/pill_wrapper" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/toolbar" - mozac:awesomeBarChipBackgroundColor="@color/photonGreen50" + app:layout_constraintTop_toBottomOf="@id/top_barrier" mozac:awesomeBarDescriptionTextColor="?secondaryText" mozac:awesomeBarTitleTextColor="?primaryText" /> @@ -55,7 +54,15 @@ android:layout="@layout/search_suggestions_hint" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toBottomOf="@id/toolbar" /> + + + - + - + + + + + android:textSize="18sp" + app:layout_constraintBottom_toTopOf="@+id/recently_closed_tabs_description" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/icon" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" + app:layout_goneMarginEnd="@dimen/library_item_icon_margin_horizontal" /> + + + + + - diff --git a/app/src/main/res/layout/search_engine_radio_button.xml b/app/src/main/res/layout/search_engine_radio_button.xml index a2d1026c1..d352591af 100644 --- a/app/src/main/res/layout/search_engine_radio_button.xml +++ b/app/src/main/res/layout/search_engine_radio_button.xml @@ -14,8 +14,8 @@ android:focusable="true"> @@ -24,7 +24,7 @@ android:importantForAccessibility="no" android:layout_width="@dimen/search_engine_engine_icon_height" android:layout_height="@dimen/search_engine_engine_icon_height" - android:layout_marginStart="@dimen/search_bar_search_icon_margin" + android:layout_marginStart="@dimen/search_engine_engine_icon_margin" android:layout_gravity="center" /> - - + android:importantForAccessibility="no" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@id/favicon_image" + tools:ignore="SmallSp" /> + + + + + - + diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 866cdf46b..a91a57421 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -31,6 +31,10 @@ android:id="@+id/action_global_search_dialog" app:destination="@id/searchDialogFragment" /> + + @@ -177,6 +181,11 @@ app:argType="org.mozilla.fenix.components.metrics.Event$PerformedSearch$SearchAccessPoint" /> + + Abrir los enllaces nuna llingüeta privada Permitir la fechura de captures nel restolar en privao + + Si s\'activa, les llingüetes privaes tamién van ser visibles cuando s\'abran munches aplicaciones Amestar un atayu pa restolar en privao @@ -281,6 +283,8 @@ Estilu Aniciu + + Xestos Personalización @@ -1228,7 +1232,7 @@ Nome del atayu - Pues amestar fácilmente esti sitiu web a la pantalla d\'aniciu del preséu p\'acceder nel intre y restolalu como si fore una aplicación. + Pues amestar fácilmente esti sitiu web a la pantalla d\'Aniciu del preséu p\'acceder aína a elli como si fore una aplicación nativa. Anicios de sesión y contraseñes @@ -1462,8 +1466,6 @@ Algamóse la llende de sitios destacaos - - P\'amestar sitios nuevos desanicia dalgún. Ten primíu un sitiu y esbilla Desaniciar. Val, entendílo @@ -1478,4 +1480,7 @@ Coleiciona les coses que t\'importen Agrupa busques, sitios y llingüetes asemeyaos p\'acceder aína a ellos dempués. + + Pues amestar fácilmente esti sitiu web a la pantalla d\'aniciu del preséu p\'acceder nel intre y restolalu como si fore una aplicación. + diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 4e0f89dd2..68a9d38f2 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -402,6 +402,8 @@ Доследы + + Дазваляе Mozilla ўсталёўваць эксперыментальныя функцыі і збіраць звесткі для іх Паведамляльнік пра крахі @@ -934,6 +936,9 @@ Аўтаматычны памер шрыфту + + Памер шрыфту будзе адпавядаць наладам Android. Адключыце, каб кіраваць шрыфтам тут. + Выдаліць дадзеныя аглядання @@ -1184,6 +1189,8 @@ Майнеры крыптавалют Збіральнікі лічбавых адбіткаў + + Спыняе збор унікальнай ідэнтыфікацыйнай інфармацыі пра вашу прыладу, якая можа быць выкарыстана для сачэння. Змест з элементамі сачэння @@ -1354,6 +1361,9 @@ Апошняе выкарыстанне + + Меню сартавання лагінаў + Дадаць пашукавік @@ -1474,7 +1484,7 @@ Лагін з такім імем карыстальніка ўжо існуе - + Падключыць іншую прыладу. diff --git a/app/src/main/res/values-cak/strings.xml b/app/src/main/res/values-cak/strings.xml index 671831e2e..3e6e4efe7 100644 --- a/app/src/main/res/values-cak/strings.xml +++ b/app/src/main/res/values-cak/strings.xml @@ -1081,7 +1081,7 @@ - Xatikirisaj molojri\'ïl pa jun chik rokik\'amaya\'l Firefox achi\'el %s pa re oyonib\'äl re\'. ¿La nawajo\' natikirisaj molojri\'ïl rik\'in re rub\'i\' taqoya\'l re\'? + Xatikirisaj molojri\'ïl pa jun chik rokik\'amaya\'l Firefox achi\'el %s pa re okisab\'äl re\'. ¿La nawajo\' natikirisaj molojri\'ïl rik\'in re rub\'i\' taqoya\'l re\'? Ja\', titikirisäx molojri\'ïl @@ -1333,7 +1333,7 @@ Rub\'i\' ri choj okem - Anin yatikïr naya\' re ajk\'amaya\'l pa ri Rutikirib\'al ruxaq awoyonib\'al richin aninäq nawokisaj chuqa\' aninäq yatok pa k\'amaya\'l, achi\'el ta xa jun chokoy. + Anin yatikïr naya\' re ajk\'amaya\'l pa ri Rutikirib\'al ruxaq okisab\'al richin aninäq nawokisaj chuqa\' aninäq yatok pa k\'amaya\'l, achi\'el ta xa jun chokoy. Kitikirisaxik molojri\'ïl chuqa\' ewan taq tzij @@ -1403,8 +1403,12 @@ Xwachib\'ëx ri ruxaq pa molwuj Tiwachib\'ëx ewan tzij + + Tijosq\'iï ewan tzij Tiwachib\'ëx rub\'i\' winäq + + Tijosq\'ïx rub\'i\' winäq Tiwachib\'ëx ruxaq @@ -1565,7 +1569,7 @@ Achi\'el: \nhttps://www.google.com/search?q=%s K\'o chik jun tikirib\'äl molojri\'ïl rik\'in re b\'i\'aj re\' - + Tokisäx jun chik okisab\'äl Tajuxub\'ej chik awi\'. @@ -1585,8 +1589,6 @@ Achi\'el: \nhttps://www.google.com/search?q=%s Xaq\'i\' ruchi\' ri jutaqil taq ruxaq - - Richin natz\'aqatisaj jun k\'ak\'a\' jutaqil ruxaq, tayuju\' jun. Tapitz\'a\' pa ruwi\' ri ruxaq richin nacha\' tiyuj. ÜTZ, Wetaman Chik @@ -1604,4 +1606,9 @@ Achi\'el: \nhttps://www.google.com/search?q=%s Ke\'amolo\' ri taq wachinäq niqa chawa Ketzob\'ajïx taq kanoxïk, taq ruxaq chuqa\' taq ruwi\' ejunam richin ye\'okisäx na. + + Xatikirisaj molojri\'ïl pa jun chik rokik\'amaya\'l Firefox achi\'el %s pa re oyonib\'äl re\'. ¿La nawajo\' natikirisaj molojri\'ïl rik\'in re rub\'i\' taqoya\'l re\'? + + Anin yatikïr naya\' re ajk\'amaya\'l pa ri Rutikirib\'al ruxaq awoyonib\'al richin aninäq nawokisaj chuqa\' aninäq yatok pa k\'amaya\'l, achi\'el ta xa jun chokoy. + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index cc2f2298d..1b7a3e67d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -517,7 +517,7 @@ - Zavřít panely + Zavírat panely Ručně @@ -1064,10 +1064,6 @@ S účtem Firefoxu můžete synchronizovat záložky, hesla i další svá data. Zjistit více - - V dalším prohlížeči Firefox na tomto telefonu už jste přihlášení jako %s. Chcete se přihlásit tímto účtem? Ano, přihlásit @@ -1309,9 +1305,6 @@ Název zkratky - - Tuto stránku si můžete snadno přidat na domovskou obrazovku vašeho telefonu. Budete k ní mít okamžitý přístup a prohlížení bude rychlejší se zážitkem jako v aplikaci. - Přihlašovací údaje @@ -1540,7 +1533,7 @@ Přihlašovací údaje s tímto uživatelským jménem už existují - + Připojte další zařízení. @@ -1560,8 +1553,6 @@ Dosažen limit počtu top stránek - - Pro přidání další top stránky nejdříve nějakou odeberte. Stačí na ní podržet prst. OK, rozumím @@ -1576,4 +1567,9 @@ Uložte si důležité věci do sbírek Podobná vyhledávání, stránky a panely si můžete seskupit a poté se k nim snadno vracet. + + V dalším prohlížeči Firefox na tomto telefonu už jste přihlášení jako %s. Chcete se přihlásit tímto účtem? + + Tuto stránku si můžete snadno přidat na domovskou obrazovku vašeho telefonu. Budete k ní mít okamžitý přístup a prohlížení bude rychlejší se zážitkem jako v aplikaci. + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 036a7a922..5ff39f3af 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -273,6 +273,8 @@ Links in privatem Tab öffnen Bildschirmfotos im privaten Modus zulassen + + Wenn erlaubt, sind auch private Tabs sichtbar, wenn mehrere Apps geöffnet sind Verknüpfung zum privaten Modus hinzufügen @@ -293,6 +295,8 @@ Theme Startseite + + Gesten Anpassen @@ -471,6 +475,16 @@ Geräte-Theme beachten + + + Zum Aktualisieren ziehen + + Zum Ausblenden der Symbolleiste scrollen + + Symbolleiste zur Seite wischen, um Tabs zu wechseln + + Nach oben wischen, ob Tabs zu öffnen + Sitzungen @@ -1086,7 +1100,7 @@ - Sie sind in einem anderen Firefox-Browser auf diesem Handy als %s angemeldet. Möchten Sie sich mit diesem Konto anmelden? + Sie sind in einem anderen Firefox-Browser auf diesem Gerät als %s angemeldet. Möchten Sie sich mit diesem Konto anmelden? Ja, anmelden @@ -1330,7 +1344,7 @@ Name der Verknüpfung - Sie können diese Website einfach zum Startbildschirm Ihres Handys hinzufügen, um unmittelbaren Zugriff darauf zu haben und sie wie eine App zu nutzen. + Sie können diese Website einfach zum Startbildschirm Ihres Geräts hinzufügen, um unmittelbaren Zugriff darauf zu haben und sie wie eine App zu nutzen. Zugangsdaten und Passwörter @@ -1400,8 +1414,12 @@ Website in Zwischenablage kopiert Passwort kopieren + + Passwort löschen Benutzernamen kopieren + + Benutzername löschen Website kopieren @@ -1557,7 +1575,7 @@ Es existieren bereits Zugangsdaten mit diesem Benutzernamen - + Weiteres Gerät verbinden. Bitte erneut authentifizieren. @@ -1577,7 +1595,7 @@ Obergrenze für wichtige Seiten erreicht - Entfernen Sie eine der wichtigen Seiten, um eine neue hinzuzufügen. Tippen Sie lange auf die Seite und wählen Sie „Entfernen“. + Entfernen Sie eine der wichtigen Seiten, um eine neue hinzuzufügen. Tippen und halten Sie die Seite und wählen Sie „Entfernen“. Ok, verstanden @@ -1595,4 +1613,9 @@ Sammeln Sie die Dinge, die Ihnen wichtig sind Gruppieren Sie ähnliche Suchanfragen, Websites und Tabs, um später schnell darauf zugreifen zu können. - + + Sie sind in einem anderen Firefox-Browser auf diesem Handy als %s angemeldet. Möchten Sie sich mit diesem Konto anmelden? + + Sie können diese Website einfach zum Startbildschirm Ihres Handys hinzufügen, um unmittelbaren Zugriff darauf zu haben und sie wie eine App zu nutzen. + + diff --git a/app/src/main/res/values-en-rCA/strings.xml b/app/src/main/res/values-en-rCA/strings.xml index 0ddf9d607..956330551 100644 --- a/app/src/main/res/values-en-rCA/strings.xml +++ b/app/src/main/res/values-en-rCA/strings.xml @@ -266,6 +266,8 @@ Open links in a private tab Allow screenshots in private browsing + + If allowed, private tabs will also be visible when multiple apps are open Add private browsing shortcut @@ -286,6 +288,8 @@ Theme Home + + Gestures Customize @@ -458,6 +462,16 @@ Follow device theme + + + Pull to refresh + + Scroll to hide toolbar + + Swipe toolbar sideways to switch tabs + + Swipe toolbar up to open tabs + Sessions @@ -1044,7 +1058,7 @@ - You are signed in as %s on another Firefox browser on this phone. Would you like to sign in with this account? + You are signed in as %s on another Firefox browser on this device. Would you like to sign in with this account? Yes, sign me in @@ -1287,7 +1301,7 @@ Shortcut name - You can easily add this website to your phone’s Home screen to have instant access and browse faster with an app-like experience. + You can easily add this website to your device’s Home screen to have instant access and browse faster with an app-like experience. Logins and passwords @@ -1357,8 +1371,12 @@ Site copied to clipboard Copy password + + Clear password Copy username + + Clear username Copy site @@ -1513,7 +1531,7 @@ A login with that username already exists - + Connect another device. Please re-authenticate. @@ -1533,7 +1551,7 @@ Top site limit reached - To add a new top site, remove one. Long press the site and select remove. + To add a new top site, remove one. Touch and hold the site and select remove. OK, Got It @@ -1551,4 +1569,9 @@ Collect the things that matter to you Group together similar searches, sites, and tabs for quick access later. - + + You are signed in as %s on another Firefox browser on this phone. Would you like to sign in with this account? + + You can easily add this website to your phone’s Home screen to have instant access and browse faster with an app-like experience. + + diff --git a/app/src/main/res/values-hsb/strings.xml b/app/src/main/res/values-hsb/strings.xml index bf8834b98..bae32dc4c 100644 --- a/app/src/main/res/values-hsb/strings.xml +++ b/app/src/main/res/values-hsb/strings.xml @@ -268,6 +268,8 @@ Wotkazy w priwatnym rajtarku wočinić Fota wobrazowki w priwatnym modusu dowolić + + Jeli dowolene, budu priwatne rajtarki tež widźomne, hdyž wjacore nałoženja su wočinjene Skrótšenku za priwatny modus přidać @@ -288,6 +290,8 @@ Drasta Startowa strona + + Gesty Přiměrić @@ -462,6 +466,17 @@ Na gratowu drastu dźiwać + + + Ćehńće, zo byšće aktualizował + + Kulće, zo byšće symbolowu lajstu schował + + + Trějće symbolowu lajstu nabok, zo byšće rajtarki přepinał + + Trějće symbolowu lajstu horje, zo byšće rajtarki wočinił + Posedźenja @@ -1053,7 +1068,7 @@ - Sće so jako %s w druhim wobhladowaku Firefox na tutym telefonje přizjewił. Chceće so z tutym kontom přizjewić? + Sće so jako %s w druhim wobhladowaku Firefox na tutym graće přizjewił. Chceće so z tutym kontom přizjewić? Haj, přizjewić @@ -1298,7 +1313,7 @@ Mjeno skrótšenki - Móžeće startowej wobrazowce swojeho telefona tute websydło lochko přidać, zo byšće direktny přistup měł a spěšnišo z dožiwjenjom nałoženja přehladował. + Móžeće startowej wobrazowce swojeho grata tute websydło lochko přidać, zo byšće direktny přistup měł a spěšnišo z dožiwjenjom nałoženja přehladował. Přizjewjenja a hesła @@ -1368,8 +1383,12 @@ Sydło je so do mjezyskłada kopěrowało Hesło kopěrować + + Hesło zhašeć Wužiwarske mjeno kopěrować + + Wužiwarske mjeno zhašeć Sydło kopěrować @@ -1527,7 +1546,7 @@ Přizjewjenje z tym wužiwarskim mjenom hižo eksistuje. - + Z druhim gratom zwjazać. Prošu awtentifikujće znowa. @@ -1547,7 +1566,7 @@ Limit za wažne sydła docpěty - Zo byšće nowe wažne sydło přidał, wotstrońće jedne z nich. Tłóčće dołho na sydło a wubjerće „Wotstronić“. + Zo byšće nowe wažne sydło přidał, wotstrońće jedne z nich. Dótkńće so sydła, dźeržće jo a wubjerće „Wotstronić“. W porjadku, sym zrozumił @@ -1565,4 +1584,9 @@ Zběrajće wěcy, kotrež su wam wažne Zeskupće podobne pytanja, sydła a rajtarki za pozdźiši spěšny přistup. - + + Sće so jako %s w druhim wobhladowaku Firefox na tutym telefonje přizjewił. Chceće so z tutym kontom přizjewić? + + Móžeće startowej wobrazowce swojeho telefona tute websydło lochko přidać, zo byšće direktny přistup měł a spěšnišo z dožiwjenjom nałoženja přehladował. + + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index e075402ab..ec7610e97 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -269,6 +269,8 @@ Hivatkozások megnyitása privát lapon Képernyőképek engedélyezése privát böngészésben + + Ha engedélyezett, a privát lapok akkor is láthatóak lesznek, ha több alkalmazás van nyitva Privát böngészési parancsikon hozzáadása @@ -289,6 +291,8 @@ Téma Kezdőlap + + Kézmozdulatok Testreszabás @@ -465,6 +469,17 @@ Az eszköz témájának követése + + + Húzza a frissítéshez + + Görgessen az eszköztár elrejtéséhez + + + Seperje oldalra az eszköztárat a lapok közti váltáshoz + + Seperje felfelé az eszköztárat a lapok megnyitásához + Munkamenetek @@ -1061,7 +1076,7 @@ - A következőként van bejelentkezve egy másik Firefox böngészőben ezen a telefonon: %s. Szeretne bejelentkezni ezzel a fiókkal? + A következőként van bejelentkezve egy másik Firefox böngészőben ezen az eszközön: %s. Szeretne bejelentkezni ezzel a fiókkal? Igen, jelentkeztessen be @@ -1308,7 +1323,7 @@ Parancsikon neve - Könnyedén hozzáadhatja ezt a weboldalt a telefonja Kezdőképernyőhöz, és azonnal elérheti azt, így gyorsabban böngészve, miközben alkalmazásszerű élményt kap. + Könnyedén hozzáadhatja ezt a weboldalt az eszköze Kezdőképernyőhöz, és azonnal elérheti azt, így gyorsabban böngészve, miközben alkalmazásszerű élményt kap. Bejelentkezések és jelszavak @@ -1378,8 +1393,12 @@ Az oldal vágólapra másolva Jelszó másolása + + Jelszó törlése Felhasználónév másolása + + Felhasználónév törlése Oldal másolása @@ -1535,7 +1554,7 @@ Már létezik bejelentkezés ezzel a felhasználónévvel. - + Másik eszköz csatlakoztatása. Hitelesítsen újra. @@ -1556,7 +1575,7 @@ Kedvenc oldalak korlátja elérve - Új kedvenc oldal hozzáadásához távolítson el egyet. Nyomja hosszan az oldalt, és válassza az eltávolítást. + Új kedvenc oldal hozzáadásához távolítson el egyet. Érintse meg és tartsa az ujját az oldalon, és válassza az eltávolítást. Rendben, értem @@ -1574,4 +1593,9 @@ Gyűjtse össze az Önnek fontos dolgokat Csoportosítsa a hasonló kereséseket, webhelyeket és lapokat a későbbi gyors elérés érdekében. - + + A következőként van bejelentkezve egy másik Firefox böngészőben ezen a telefonon: %s. Szeretne bejelentkezni ezzel a fiókkal? + + Könnyedén hozzáadhatja ezt a weboldalt a telefonja Kezdőképernyőhöz, és azonnal elérheti azt, így gyorsabban böngészve, miközben alkalmazásszerű élményt kap. + + diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index d2a120157..50075d391 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -265,6 +265,8 @@ פתיחת קישורים בלשונית פרטית לאפשר צילומי מסך בגלישה פרטית + + אם אפשרות זו מופעלת, לשוניות פרטיות תהיינה מוצגות כאשר מספר יישומונים פתוחים הוספת קיצור דרך לגלישה פרטית @@ -283,6 +285,8 @@ ערכת נושא בית + + מחוות התאמה אישית @@ -455,6 +459,16 @@ שימוש בערכת הנושא של המכשיר + + + משיכה לרענון הדף + + גלילה להסתרת סרגל הכלים + + החלקה על סרגל הכלים כלפי הצדדים למעבר בין לשוניות + + החלקה על סרגל הכלים כלפי מעלה לפתיחת לשוניות + הפעלות @@ -1046,7 +1060,7 @@ - דפדפן Firefox נוסף בטלפון זה מחובר כ־%s. האם ברצונך להתחבר עם חשבון זה? + דפדפן Firefox נוסף במכשיר זה מחובר כ־%s. האם ברצונך להתחבר עם חשבון זה? כן, תחברו אותי @@ -1288,7 +1302,7 @@ שם קיצור הדרך - באפשרותך להוסיף בקלות אתר זה למסך הבית של הטלפון שלך כדי לקבל גישה מיידית ולגלוש מהר יותר עם חוויה שמדמה שימוש ביישומון. + באפשרותך להוסיף בקלות אתר זה למסך הבית של המכשיר שלך כדי לקבל גישה מיידית ולגלוש מהר יותר עם חוויה שמדמה שימוש ביישומון. כניסות וססמאות @@ -1350,8 +1364,12 @@ האתר הועתק ללוח העתקת ססמה + + ניקוי ססמה העתקת שם משתמש + + ניקוי שם משתמש העתקת אתר @@ -1503,7 +1521,7 @@ כבר קיימת כניסה עם שם משתמש זה - + נא לחבר מכשיר נוסף. נא להפעיל סנכרון לשוניות. @@ -1522,7 +1540,7 @@ הגעת למכסת האתרים המובילים - כדי להוסיף אתר מוביל חדש, יש להסיר אחד אחר. יש ללחוץ לחיצה ארוכה על האתר ולבחור ב״הסרה״. + כדי להוסיף אתר מוביל חדש, יש להסיר אחד אחר. יש ללחוץ לחיצה ארוכה על האתר ולבחור ב״הסרה״. בסדר, הבנתי @@ -1540,4 +1558,9 @@ לאסוף את הדברים החשובים לך ניתן לקבץ חיפושים, אתרים ולשוניות דומים יחד כדי לגשת אליהם מהר יותר בהמשך. - + + דפדפן Firefox נוסף בטלפון זה מחובר כ־%s. האם ברצונך להתחבר עם חשבון זה? + + באפשרותך להוסיף בקלות אתר זה למסך הבית של הטלפון שלך כדי לקבל גישה מיידית ולגלוש מהר יותר עם חוויה שמדמה שימוש ביישומון. + + diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index c429ec348..8eb481a27 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -278,6 +278,8 @@ 사생활 보호 탭에 링크 열기 사생활 보호 모드에서 스크린샷 허용 + + 허용되는 경우, 여러 앱이 열려있을 때 사생활 보호 탭도 표시됨 사생활 보호 모드 바로 가기 추가 @@ -298,6 +300,8 @@ 테마 + + 제스처 사용자 지정 @@ -476,6 +480,16 @@ 기기 테마 따르기 + + + 당겨서 새로 고침 + + 스크롤해서 툴바 숨기기 + + 도구 모음을 옆으로 밀어서 탭 전환 + + 도구 모음을 위로 밀어서 탭 열기 + 세션 @@ -1097,7 +1111,7 @@ - 이 휴대폰의 다른 Firefox 브라우저에서 %s로 로그인했습니다. 이 계정으로 로그인하시겠습니까? + 이 기기의 다른 Firefox 브라우저에서 %s(으)로 로그인했습니다. 이 계정으로 로그인하시겠습니까? 예, 로그인함 @@ -1345,7 +1359,7 @@ 바로 가기 이름 - 이 웹 사이트를 휴대폰의 홈 화면에 쉽게 추가하여 앱과 같은 경험을 통해 즉시 액세스하고 더 빠르게 탐색 할 수 있습니다. + 이 웹 사이트를 기기의 홈 화면에 쉽게 추가하여 앱과 같은 경험을 통해 즉시 액세스하고 더 빠르게 탐색 할 수 있습니다. 로그인과 비밀번호 @@ -1415,8 +1429,12 @@ 사이트가 클립보드에 복사됨 비밀번호 복사 + + 비밀번호 지우기 사용자 이름 복사 + + 사용자 이름 지우기 사이트 복사 @@ -1574,7 +1592,7 @@ 해당 사용자 이름을 가진 로그인이 이미 존재합니다 - + 다른 기기를 연결하세요. 다시 인증하세요. @@ -1595,7 +1613,7 @@ 상위 사이트 제한에 도달 - 새로운 상위 사이트를 추가하려면 삭제하세요. 사이트를 길게 누르고 삭제를 선택하세요. + 새 상위 사이트를 추가하려면 하나를 삭제하세요. 사이트를 길게 터치하고 삭제를 선택하세요. 확인 @@ -1613,4 +1631,9 @@ 중요한 것들 수집하기 나중에 빠르게 접근할 수 있도록 유사한 검색, 사이트 및 탭을 모아 보세요. - + + 이 휴대폰의 다른 Firefox 브라우저에서 %s(으)로 로그인했습니다. 이 계정으로 로그인하시겠습니까? + + 이 웹 사이트를 휴대폰의 홈 화면에 쉽게 추가하여 앱과 같은 경험을 통해 즉시 액세스하고 더 빠르게 탐색 할 수 있습니다. + + diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index a42805199..96307fcf3 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -290,6 +290,8 @@ Tema Hjem + + Bevegelser Tilpass @@ -466,6 +468,10 @@ Følg enhetens tema + + + Trekk for å oppdatere + Økter @@ -1059,10 +1065,6 @@ Begynn å synkronisere bokmerker, passord og mer med Firefox-kontoen din. Les mer - - Du er logget inn som %s på en annen Firefox-nettleser på denne telefonen. Vil du logge inn med denne kontoen? Ja, logg meg inn @@ -1313,9 +1315,6 @@ Navn på snarvei - - Du kan enkelt legge til dette nettstedet på telefonens startskjermen for å få øyeblikkelig tilgang og surfe raskere med en app-lignende opplevelse. - Innlogginger og passord @@ -1547,7 +1546,7 @@ En innlogging med det brukernavnet eksisterer allerede - + Koble til en annen enhet. Autentiser på nytt. @@ -1566,8 +1565,6 @@ Grense for populære nettsteder nådd - - For å legge til et nytt populært nettsted må du fjern en annen. Trykk lenge på nettstedet og velg fjern. OK, jeg skjønner @@ -1585,4 +1582,9 @@ Samle tingene som betyr noe for deg Grupper sammen lignende søk, nettsteder og faner for rask tilgang senere. - + + Du er logget inn som %s på en annen Firefox-nettleser på denne telefonen. Vil du logge inn med denne kontoen? + + Du kan enkelt legge til dette nettstedet på telefonens startskjermen for å få øyeblikkelig tilgang og surfe raskere med en app-lignende opplevelse. + + diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index b1952049a..12411a838 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -268,6 +268,8 @@ Abrir links em abas privativas Permitir capturas de tela na navegação privativa + + Se permitido, as abas privativas também ficam visíveis quando vários aplicativos estão abertos Adicionar atalho para navegação privativa @@ -288,6 +290,8 @@ Tema Tela inicial + + Gestos Personalizar @@ -462,6 +466,16 @@ Acompanhar tema do dispositivo + + + Puxe para atualizar + + Deslize para ocultar a barra de ferramentas + + Deslize a barra de ferramentas para o lado para mudar de aba + + Deslize a barra de ferramentas para cima para abrir abas + Sessões @@ -1052,7 +1066,7 @@ - Você está conectado como %s em outro navegador Firefox neste celular. Quer entrar com esta conta? + Você está conectado como %s em outro navegador Firefox neste dispositivo. Quer entrar com esta conta? Sim, entrar @@ -1299,7 +1313,7 @@ Nome do atalho - Você pode facilmente adicionar este site à tela inicial do celular para ter acesso imediato e navegar mais rápido com uma experiência semelhante a um aplicativo. + Você pode facilmente adicionar este site à tela inicial do dispositivo para ter acesso imediato e navegar mais rápido com uma experiência semelhante a um aplicativo. Contas e senhas @@ -1369,8 +1383,12 @@ Site copiado para a área de transferência Copiar senha + + Limpar senha Copiar nome de usuário + + Limpar nome de usuário Copiar site @@ -1526,7 +1544,7 @@ Já existe uma conta com este nome de usuário - + Conecte outro dispositivo. Autentique novamente. @@ -1547,7 +1565,7 @@ Atingiu o limite de sites preferidos - Para adicionar mais um site preferido, remova outro. Mantenha o dedo sobre o site e selecione remover. + Para adicionar mais um site preferido, remova outro. Mantenha o dedo sobre o site e selecione remover. OK, entendi @@ -1565,4 +1583,9 @@ Reúna o que é importante para você Agrupe pesquisas, sites e abas semelhantes para acesso rápido mais tarde. - + + Você está conectado como %s em outro navegador Firefox neste celular. Quer entrar com esta conta? + + Você pode facilmente adicionar este site à tela inicial do celular para ter acesso imediato e navegar mais rápido com uma experiência semelhante a um aplicativo. + + diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 7a26f9e2a..1ec9e82ed 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -272,6 +272,8 @@ Öppna länkar i en privat flik Tillåt skärmdumpar i privat surfning + + Om tillåtet kommer privata flikar också att visas när flera appar är öppna Lägg till genväg för privat surfning @@ -292,6 +294,8 @@ Tema Hem + + Gester Anpassa @@ -470,6 +474,16 @@ Följ enhetens tema + + + Dra för att uppdatera + + Bläddra för att dölja verktygsfältet + + Svep verktygsfältet i sidled för att byta flik + + Svep verktygsfältet uppåt för att öppna flikar + Sessioner @@ -1068,7 +1082,7 @@ - Du är inloggad som %s på en annan Firefox-webbläsare på den här telefonen. Vill du logga in med det här kontot? + Du är inloggad som %s i en annan Firefox-webbläsare på den här enheten. Vill du logga in med det här kontot? Ja, logga in mig @@ -1315,7 +1329,7 @@ Genvägens namn - Du kan enkelt lägga till den här webbplatsen på telefonens startsida för att få direktåtkomst och surfa snabbare med en appliknande upplevelse. + Du kan enkelt lägga till den här webbplatsen på enhetens startskärm för att få direktåtkomst och surfa snabbare med en appliknande upplevelse. Inloggningar och lösenord @@ -1385,8 +1399,12 @@ Webbplats kopierad till urklipp Kopiera lösenord + + Rensa lösenord Kopiera användarnamn + + Rensa användarnamn Kopiera webbplats @@ -1542,7 +1560,7 @@ En inloggning med det användarnamnet finns redan - + Anslut en annan enhet. Bekräfta igen. @@ -1562,7 +1580,7 @@ Övre gräns för mest besökta nådd - För att lägga till en ny mest besökt sida, ta bort en. Tryck länge på webbplatsen och välj ta bort. + För att lägga till en ny mest besökt sida, ta bort en. Tryck länge på webbplatsen och välj ta bort. Ok, jag förstår @@ -1580,4 +1598,9 @@ Samla de saker som är viktiga för dig Gruppera liknande sökningar, webbplatser och flikar för snabb åtkomst senare. - + + Du är inloggad som %s på en annan Firefox-webbläsare på den här telefonen. Vill du logga in med det här kontot? + + Du kan enkelt lägga till den här webbplatsen på telefonens startsida för att få direktåtkomst och surfa snabbare med en appliknande upplevelse. + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 7e3c33476..4b188525c 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -22,6 +22,7 @@ 24dp 12dp 28dp + 12dp 48dp 24dp 32dp @@ -176,7 +177,8 @@ 40dp 4dp 64dp - 12dp + 8dp + 12dp 8dp diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 285471310..f5d6d523b 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -97,6 +97,7 @@ pref_key_show_search_suggestions_in_private pref_key_show_search_suggestions_in_privateonboarding pref_key_show_voice_search + pref_key_enable_domain_autocomplete pref_key_optimize @@ -241,4 +242,6 @@ pref_key_close_tabs_after_one_day pref_key_close_tabs_after_one_week pref_key_close_tabs_after_one_month + + pref_key_camera_permissions diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d0509f4ce..5a2a2034a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -328,6 +328,8 @@ Search bookmarks Account settings + + Autocomplete URLs Open links in apps @@ -1591,7 +1593,7 @@ Remove - Get the most out of %s. diff --git a/app/src/main/res/xml/search_preferences.xml b/app/src/main/res/xml/search_preferences.xml index d892037e0..9d5cc495d 100644 --- a/app/src/main/res/xml/search_preferences.xml +++ b/app/src/main/res/xml/search_preferences.xml @@ -28,6 +28,10 @@ android:defaultValue="true" android:key="@string/pref_key_show_search_engine_shortcuts" android:title="@string/preferences_show_search_engines" /> + { + + override fun match(arg: NavOptions?): Boolean = + value.popUpTo == arg?.popUpTo && value.isPopUpToInclusive == arg.isPopUpToInclusive + + override fun substitute(map: Map) = + copy(value = value.internalSubstitute(map)) +} + private data class EqIntentFilterMatcher(private val value: Intent) : Matcher { override fun match(arg: Intent?): Boolean = value.filterEquals(arg) diff --git a/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/DefaultRecentlyClosedControllerTest.kt b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/DefaultRecentlyClosedControllerTest.kt new file mode 100644 index 000000000..1d2a767a1 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/DefaultRecentlyClosedControllerTest.kt @@ -0,0 +1,171 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.library.recentlyclosed + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.res.Resources +import androidx.navigation.NavController +import androidx.navigation.NavOptions +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.slot +import io.mockk.unmockkStatic +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.state.action.RecentlyClosedAction +import mozilla.components.browser.state.state.ClosedTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.prompt.ShareData +import mozilla.components.feature.recentlyclosed.ext.restoreTab +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.R +import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.components.FenixSnackbar +import org.mozilla.fenix.ext.directionsEq +import org.mozilla.fenix.ext.optionsEq +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +// Robolectric needed for `onShareItem()` +@ExperimentalCoroutinesApi +@RunWith(FenixRobolectricTestRunner::class) +class DefaultRecentlyClosedControllerTest { + private val dispatcher = TestCoroutineDispatcher() + private val navController: NavController = mockk(relaxed = true) + private val resources: Resources = mockk(relaxed = true) + private val snackbar: FenixSnackbar = mockk(relaxed = true) + private val clipboardManager: ClipboardManager = mockk(relaxed = true) + private val openToBrowser: (ClosedTab, BrowsingMode?) -> Unit = mockk(relaxed = true) + private val sessionManager: SessionManager = mockk(relaxed = true) + private val activity: HomeActivity = mockk(relaxed = true) + private val store: BrowserStore = mockk(relaxed = true) + val mockedTab: ClosedTab = mockk(relaxed = true) + + private val controller = DefaultRecentlyClosedController( + navController, + store, + sessionManager, + resources, + snackbar, + clipboardManager, + activity, + openToBrowser + ) + + @Before + fun setUp() { + mockkStatic("mozilla.components.feature.recentlyclosed.ext.ClosedTabKt") + every { mockedTab.restoreTab(any(), any(), any()) } just Runs + } + + @After + fun tearDown() { + dispatcher.cleanupTestCoroutines() + unmockkStatic("mozilla.components.feature.recentlyclosed.ext.ClosedTabKt") + } + + @Test + fun handleOpen() { + val item: ClosedTab = mockk(relaxed = true) + + controller.handleOpen(item, BrowsingMode.Private) + + verify { + openToBrowser(item, BrowsingMode.Private) + } + + controller.handleOpen(item, BrowsingMode.Normal) + + verify { + openToBrowser(item, BrowsingMode.Normal) + } + } + + @Test + fun handleDeleteOne() { + val item: ClosedTab = mockk(relaxed = true) + + controller.handleDeleteOne(item) + + verify { + store.dispatch(RecentlyClosedAction.RemoveClosedTabAction(item)) + } + } + + @Test + fun handleNavigateToHistory() { + controller.handleNavigateToHistory() + + verify { + navController.navigate( + directionsEq( + RecentlyClosedFragmentDirections.actionGlobalHistoryFragment() + ), + optionsEq(NavOptions.Builder().setPopUpTo(R.id.historyFragment, true).build()) + ) + } + } + + @Test + fun handleCopyUrl() { + val item = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L) + + val clipdata = slot() + + controller.handleCopyUrl(item) + + verify { + clipboardManager.setPrimaryClip(capture(clipdata)) + snackbar.show() + } + + assertEquals(1, clipdata.captured.itemCount) + assertEquals("mozilla.org", clipdata.captured.description.label) + assertEquals("mozilla.org", clipdata.captured.getItemAt(0).text) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun handleShare() { + val item = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L) + + controller.handleShare(item) + + verify { + navController.navigate( + directionsEq( + RecentlyClosedFragmentDirections.actionGlobalShareFragment( + data = arrayOf(ShareData(url = item.url, title = item.title)) + ) + ) + ) + } + } + + @Test + fun handleRestore() { + controller.handleRestore(mockedTab) + + dispatcher.advanceUntilIdle() + + verify { + mockedTab.restoreTab( + store, + sessionManager, + onTabRestored = any() + ) + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractorTest.kt new file mode 100644 index 000000000..c4242bc03 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractorTest.kt @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.library.recentlyclosed + +import io.mockk.mockk +import io.mockk.verify +import mozilla.components.browser.state.state.ClosedTab +import org.junit.Before +import org.junit.Test +import org.mozilla.fenix.browser.browsingmode.BrowsingMode + +class RecentlyClosedFragmentInteractorTest { + + lateinit var interactor: RecentlyClosedFragmentInteractor + private val defaultRecentlyClosedController: DefaultRecentlyClosedController = + mockk(relaxed = true) + + @Before + fun setup() { + interactor = + RecentlyClosedFragmentInteractor( + recentlyClosedController = defaultRecentlyClosedController + ) + } + + @Test + fun open() { + val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L) + interactor.restore(tab) + + verify { + defaultRecentlyClosedController.handleRestore(tab) + } + } + + @Test + fun onCopyPressed() { + val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L) + interactor.onCopyPressed(tab) + + verify { + defaultRecentlyClosedController.handleCopyUrl(tab) + } + } + + @Test + fun onSharePressed() { + val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L) + interactor.onSharePressed(tab) + + verify { + defaultRecentlyClosedController.handleShare(tab) + } + } + + @Test + fun onOpenInNormalTab() { + val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L) + interactor.onOpenInNormalTab(tab) + + verify { + defaultRecentlyClosedController.handleOpen(tab, mode = BrowsingMode.Normal) + } + } + + @Test + fun onOpenInPrivateTab() { + val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L) + interactor.onOpenInPrivateTab(tab) + + verify { + defaultRecentlyClosedController.handleOpen(tab, mode = BrowsingMode.Private) + } + } + + @Test + fun onDeleteOne() { + val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L) + interactor.onDeleteOne(tab) + + verify { + defaultRecentlyClosedController.handleDeleteOne(tab) + } + } + + @Test + fun onNavigateToHistory() { + interactor.onNavigateToHistory() + + verify { + defaultRecentlyClosedController.handleNavigateToHistory() + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/search/DefaultSearchControllerTest.kt b/app/src/test/java/org/mozilla/fenix/search/DefaultSearchControllerTest.kt index d6a223342..52b8dbbf0 100644 --- a/app/src/test/java/org/mozilla/fenix/search/DefaultSearchControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/search/DefaultSearchControllerTest.kt @@ -4,6 +4,7 @@ package org.mozilla.fenix.search +import androidx.appcompat.app.AlertDialog import androidx.navigation.NavController import androidx.navigation.NavDirections import io.mockk.MockKAnnotations @@ -13,6 +14,7 @@ import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject +import io.mockk.spyk import io.mockk.unmockkObject import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -32,6 +34,8 @@ import org.mozilla.fenix.components.metrics.MetricsUtils import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.utils.Settings +typealias AlertDialogBuilder = AlertDialog.Builder + @ExperimentalCoroutinesApi class DefaultSearchControllerTest { @@ -58,7 +62,6 @@ class DefaultSearchControllerTest { every { id } returns R.id.searchFragment } every { MetricsUtils.createSearchEvent(searchEngine, activity, any()) } returns null - controller = DefaultSearchController( activity = activity, sessionManager = sessionManager, @@ -328,4 +331,16 @@ class DefaultSearchControllerTest { verify { sessionManager.select(any()) } verify { activity.openToBrowser(from = BrowserDirection.FromSearch) } } + + @Test + fun `show camera permissions needed dialog`() { + val dialogBuilder: AlertDialogBuilder = mockk(relaxed = true) + + val spyController = spyk(controller) + every { spyController.buildDialog() } returns dialogBuilder + + spyController.handleCameraPermissionsNeeded() + + verify { dialogBuilder.show() } + } } diff --git a/app/src/test/java/org/mozilla/fenix/searchdialog/SearchDialogControllerTest.kt b/app/src/test/java/org/mozilla/fenix/searchdialog/SearchDialogControllerTest.kt index 762f5e866..d30fde735 100644 --- a/app/src/test/java/org/mozilla/fenix/searchdialog/SearchDialogControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/searchdialog/SearchDialogControllerTest.kt @@ -13,6 +13,7 @@ import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject +import io.mockk.spyk import io.mockk.unmockkObject import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -29,6 +30,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricsUtils +import org.mozilla.fenix.search.AlertDialogBuilder import org.mozilla.fenix.search.SearchFragmentAction import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.utils.Settings @@ -44,6 +46,7 @@ class SearchDialogControllerTest { @MockK(relaxed = true) private lateinit var settings: Settings @MockK private lateinit var sessionManager: SessionManager @MockK(relaxed = true) private lateinit var clearToolbarFocus: () -> Unit + @MockK(relaxed = true) private lateinit var dismissDialog: () -> Unit private lateinit var controller: SearchDialogController @@ -67,6 +70,7 @@ class SearchDialogControllerTest { navController = navController, settings = settings, metrics = metrics, + dismissDialog = dismissDialog, clearToolbarFocus = clearToolbarFocus ) } @@ -93,6 +97,17 @@ class SearchDialogControllerTest { verify { metrics.track(Event.EnteredUrl(false)) } } + @Test + fun handleBlankUrlCommitted() { + val url = "" + + controller.handleUrlCommitted(url) + + verify { + dismissDialog() + } + } + @Test fun handleSearchCommitted() { val searchTerm = "Firefox" @@ -329,4 +344,16 @@ class SearchDialogControllerTest { verify { sessionManager.select(any()) } verify { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) } } + + @Test + fun `show camera permissions needed dialog`() { + val dialogBuilder: AlertDialogBuilder = mockk(relaxed = true) + + val spyController = spyk(controller) + every { spyController.buildDialog() } returns dialogBuilder + + spyController.handleCameraPermissionsNeeded() + + verify { dialogBuilder.show() } + } } diff --git a/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt index 0d300350b..605a81492 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt @@ -13,6 +13,8 @@ import kotlinx.coroutines.GlobalScope.coroutineContext import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.runBlockingTest import mozilla.components.browser.icons.BrowserIcons +import mozilla.components.browser.state.action.RecentlyClosedAction +import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.Engine import mozilla.components.concept.storage.HistoryStorage import mozilla.components.feature.tabs.TabsUseCases @@ -31,6 +33,7 @@ class DefaultDeleteBrowsingDataControllerTest { private var removeAllTabs: TabsUseCases.RemoveAllTabsUseCase = mockk(relaxed = true) private var historyStorage: HistoryStorage = mockk(relaxed = true) private var permissionStorage: PermissionStorage = mockk(relaxed = true) + private var store: BrowserStore = mockk(relaxed = true) private var iconsStorage: BrowserIcons = mockk(relaxed = true) private val engine: Engine = mockk(relaxed = true) private lateinit var controller: DefaultDeleteBrowsingDataController @@ -40,6 +43,7 @@ class DefaultDeleteBrowsingDataControllerTest { controller = DefaultDeleteBrowsingDataController( removeAllTabs = removeAllTabs, historyStorage = historyStorage, + store = store, permissionStorage = permissionStorage, iconsStorage = iconsStorage, engine = engine, @@ -65,6 +69,7 @@ class DefaultDeleteBrowsingDataControllerTest { coVerify { engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES)) historyStorage.deleteEverything() + store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction) iconsStorage.clear() } } 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 318b761df..0e478c3ba 100644 --- a/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt @@ -18,7 +18,7 @@ import io.mockk.verifyOrder import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager -import mozilla.components.concept.engine.profiler.Profiler +import mozilla.components.concept.base.profiler.Profiler import mozilla.components.concept.tabstray.Tab import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tabs.TabsUseCases diff --git a/app/src/test/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegrationTest.kt b/app/src/test/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegrationTest.kt deleted file mode 100644 index 77c481bf2..000000000 --- a/app/src/test/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegrationTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.wifi - -import io.mockk.Called -import io.mockk.Runs -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.verify -import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action -import org.junit.Before -import org.junit.Test -import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_AUDIBLE -import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_INAUDIBLE -import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_ALLOW_ALL -import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_ALLOW_ON_WIFI -import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_ALL -import org.mozilla.fenix.utils.Settings - -class SitePermissionsWifiIntegrationTest { - - private lateinit var settings: Settings - private lateinit var wifiConnectionMonitor: WifiConnectionMonitor - private lateinit var wifiIntegration: SitePermissionsWifiIntegration - - @Before - fun setup() { - settings = mockk() - wifiConnectionMonitor = mockk(relaxed = true) - wifiIntegration = SitePermissionsWifiIntegration(settings, wifiConnectionMonitor) - - every { settings.setSitePermissionsPhoneFeatureAction(any(), any()) } just Runs - } - - @Test - fun `add and remove wifi connected listener`() { - wifiIntegration.addWifiConnectedListener() - verify { wifiConnectionMonitor.register(any()) } - - wifiIntegration.removeWifiConnectedListener() - verify { wifiConnectionMonitor.unregister(any()) } - } - - @Test - fun `start and stop wifi connection monitor`() { - wifiIntegration.start() - verify { wifiConnectionMonitor.start() } - - wifiIntegration.stop() - verify { wifiConnectionMonitor.stop() } - } - - @Test - fun `add only if autoplay is only allowed on wifi`() { - every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ALL - wifiIntegration.maybeAddWifiConnectedListener() - verify { wifiConnectionMonitor wasNot Called } - - every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ON_WIFI - wifiIntegration.maybeAddWifiConnectedListener() - verify { wifiConnectionMonitor.register(any()) } - } - - @Test - fun `listener removes itself if autoplay is not only allowed on wifi`() { - every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ALL - wifiIntegration.onWifiConnectionChanged(connected = true) - verify { wifiConnectionMonitor.unregister(any()) } - } - - @Test - fun `listener sets audible and inaudible settings to allowed on connect`() { - every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ON_WIFI - wifiIntegration.onWifiConnectionChanged(connected = true) - verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_AUDIBLE, Action.ALLOWED) } - verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_INAUDIBLE, Action.ALLOWED) } - } - - @Test - fun `listener sets audible and inaudible settings to blocked on disconnected`() { - every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ON_WIFI - wifiIntegration.onWifiConnectionChanged(connected = false) - verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_AUDIBLE, Action.BLOCKED) } - verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_INAUDIBLE, Action.BLOCKED) } - } -} diff --git a/app/src/test/java/org/mozilla/fenix/wifi/WifiConnectionMonitorTest.kt b/app/src/test/java/org/mozilla/fenix/wifi/WifiConnectionMonitorTest.kt deleted file mode 100644 index 94b8a7f10..000000000 --- a/app/src/test/java/org/mozilla/fenix/wifi/WifiConnectionMonitorTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.wifi - -import android.net.ConnectivityManager -import android.net.NetworkRequest -import io.mockk.Runs -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.mockkConstructor -import io.mockk.slot -import io.mockk.unmockkConstructor -import io.mockk.verify -import org.junit.After -import org.junit.Before -import org.junit.Test - -class WifiConnectionMonitorTest { - - private lateinit var connectivityManager: ConnectivityManager - private lateinit var wifiConnectionMonitor: WifiConnectionMonitor - - @Before - fun setup() { - mockkConstructor(NetworkRequest.Builder::class) - connectivityManager = mockk(relaxUnitFun = true) - wifiConnectionMonitor = WifiConnectionMonitor(connectivityManager) - - every { - anyConstructed().addTransportType(any()) - } answers { self as NetworkRequest.Builder } - } - - @After - fun teardown() { - unmockkConstructor(NetworkRequest.Builder::class) - } - - @Test - fun `start runs only once`() { - wifiConnectionMonitor.start() - wifiConnectionMonitor.start() - - verify(exactly = 1) { - connectivityManager.registerNetworkCallback(any(), any()) - } - } - - @Test - fun `stop only runs after start`() { - wifiConnectionMonitor.stop() - verify(exactly = 0) { - connectivityManager.unregisterNetworkCallback(any()) - } - - wifiConnectionMonitor.start() - wifiConnectionMonitor.stop() - verify { - connectivityManager.unregisterNetworkCallback(any()) - } - } - - @Test - fun `passes results from connectivity manager to observers`() { - val slot = slot() - every { connectivityManager.registerNetworkCallback(any(), capture(slot)) } just Runs - - wifiConnectionMonitor.start() - - // Immediately notifies observer when registered - val observer = mockk(relaxed = true) - wifiConnectionMonitor.register(observer) - verify { observer.onWifiConnectionChanged(connected = false) } - - // Notifies observer when network is available or lost - slot.captured.onAvailable(mockk()) - verify { observer.onWifiConnectionChanged(connected = true) } - - slot.captured.onLost(mockk()) - verify { observer.onWifiConnectionChanged(connected = false) } - } - - private fun captureNetworkCallback(): ConnectivityManager.NetworkCallback { - val slot = slot() - verify { connectivityManager.registerNetworkCallback(any(), capture(slot)) } - return slot.captured - } -} diff --git a/buildSrc/src/main/java/AndroidComponents.kt b/buildSrc/src/main/java/AndroidComponents.kt index 6cf6bff2e..7f74fa0c3 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 = "58.0.20200908130811" + const val VERSION = "59.0.20200911130559" } diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index b3f6ff907..236689878 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -45,8 +45,8 @@ object Config { } private val fennecBaseVersionCode by lazy { - val format = SimpleDateFormat("YYYYMMDDHHMMSS", Locale.US) - val cutoff = format.parse("20150801000000") + val format = SimpleDateFormat("yyyyMMddHHmmss", Locale.US) + val cutoff = format.parse("20141228000000") val build = Date() Math.floor((build.time - cutoff.time) / (1000.0 * 60.0 * 60.0)).toInt() @@ -56,6 +56,12 @@ object Config { * Generates a versionCode that follows the same rules like legacy Fennec builds. * Adapted from: * https://searchfox.org/mozilla-central/rev/34cb8d0a2a324043bcfc2c56f37b31abe7fb23a8/python/mozbuild/mozbuild/android_version_code.py + * + * There is a discrepancy between the epoch date used here (20141228) + * and the epoch used in Fennec (20150801) for historical reasons. We keep + * this discrepancy to avoid having Fenix version codes decrease. + * Note that the original Fennec implementation also had an inconsistency in + * the documented epoch date (20150901) and the effective epoch date (20150801). */ @JvmStatic fun generateFennecVersionCode(abi: String): Int { @@ -69,7 +75,7 @@ object Config { // 0111 1000 0010 tttt tttt tttt tttt txpg // // The 17 bits labelled 't' represent the number of hours since midnight on - // September 1, 2015. (2015090100 in YYYYMMMDDHH format.) This yields a + // December 28, 2014. (2014122800 in yyyyMMddHH format.) This yields a // little under 15 years worth of hourly build identifiers, since 2**17 / (366 // * 24) =~ 14.92. // diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 6c23341f9..2a355dede 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -106,6 +106,7 @@ object Deps { const val mozilla_feature_site_permissions = "org.mozilla.components:feature-sitepermissions:${Versions.mozilla_android_components}" const val mozilla_feature_readerview = "org.mozilla.components:feature-readerview:${Versions.mozilla_android_components}" const val mozilla_feature_tab_collections = "org.mozilla.components:feature-tab-collections:${Versions.mozilla_android_components}" + const val mozilla_feature_recentlyclosed = "org.mozilla.components:feature-recentlyclosed:${Versions.mozilla_android_components}" const val mozilla_feature_accounts_push = "org.mozilla.components:feature-accounts-push:${Versions.mozilla_android_components}" const val mozilla_feature_top_sites = "org.mozilla.components:feature-top-sites:${Versions.mozilla_android_components}" const val mozilla_feature_share = "org.mozilla.components:feature-share:${Versions.mozilla_android_components}" diff --git a/l10n-uplift.py b/l10n-uplift.py new file mode 100755 index 000000000..8b890d70a --- /dev/null +++ b/l10n-uplift.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 + +# Purpose: uplift (via cherry-picking) any missing commits from an l10n bot +# from 'MAIN_BRANCH' to a specified release branch. +# +# Usage examples: (append --verbose to print out detailed information) +# Dry-run (says what will happen, doesn't do any work): ./l10n-uplift.py releases/48.0 +# Uplift, actually perform the work: ./l10n-uplift.py releases/48.0 --uplift +# Process multiple branches at once: ./l10n-uplift.py releases/48.0 releases/44.0 --uplift --verbose + +# Note: there can often be conflicts between cherry-picks, to catch duplication errors, build after conflict resolution: ./gradlew assembleDebug + +import subprocess +import argparse + +# TODO don't forget to change this once we switch to 'main' or whatever other name. +MAIN_BRANCH="master" +L10N_AUTHOR="release+l10n-automation-bot@mozilla.com" + +def run_cmd_checked(*args, **kwargs): + """Run a command, throwing an exception if it exits with non-zero status.""" + kwargs["check"] = True + kwargs["capture_output"] = True + # beware! only run this script with inputs from a trusted, non-external source + kwargs["shell"] = True + try: + return subprocess.run(*args, **kwargs).stdout.decode() + except subprocess.CalledProcessError as err: + print(err.stderr) + raise err + +def uplift_commits(branch, verbose, uplift): + print(f"\nProcessing l10n commits for '{branch}'...") + # if necessary, this will setup 'branch' to track its upstream equivalent + run_cmd_checked([f"git checkout {branch}"]) + # get l10n commits which happened on MAIN_BRANCH since 'branch' split off + commits_since_split = run_cmd_checked([f"git rev-list {branch}..{MAIN_BRANCH} --author={L10N_AUTHOR}"]).split() + # order commits by oldest-first, e.g. how we'd cherry pick them + commits_since_split.reverse() + print(f"Since '{branch}' split off '{MAIN_BRANCH}', there were {len(commits_since_split)} commit(s) from {L10N_AUTHOR}.") + + if verbose: + print(f"\nHashes of those commits on '{MAIN_BRANCH}' are: {commits_since_split}\n") + + # look for 'cherry picked' commits, and get the original commit hash from the commit message (as left by 'cherry-pick -x') + commits_already_uplifted = run_cmd_checked([f"git rev-list {MAIN_BRANCH}..{branch} --author={L10N_AUTHOR} --grep=\"cherry picked\" --pretty=%b | grep cherry | cut -d' ' -f5 | cut -c 1-40"]).split() + commits_already_uplifted.reverse() + + print(f"Of those, {len(commits_already_uplifted)} commit(s) already uplifted.") + + if verbose: + print(f"Hashes of commits already uplifted to '{branch}': {commits_already_uplifted}\n") + + commits_to_uplift = [commit for commit in commits_since_split if commit not in commits_already_uplifted] + + print(f"Need to uplift {len(commits_to_uplift)} commit(s).") + + if verbose: + print(f"Hashes of commits to uplift from '{MAIN_BRANCH}' to '{branch}': {commits_to_uplift}\n") + + if len(commits_to_uplift) == 0: + print("Nothing to uplift.") + return + + if uplift: + print(f"Uplifting (for real)...") + else: + print(f"Uplifting (dry-run)...") + + run_cmd_checked([f"git checkout {branch}"]) + for commit in commits_to_uplift: + if verbose: + print(f"Cherry picking {commit} from '{MAIN_BRANCH}' to '{branch}'") + if uplift: + run_cmd_checked([f"git cherry-pick {commit} -x"]) + if uplift: + print(f"Uplifted {len(commits_to_uplift)} commits from '{MAIN_BRANCH}' to '{branch}'") + +parser = argparse.ArgumentParser(description=f"Uplift l10n commits from {MAIN_BRANCH} to specified branches") +parser.add_argument( + 'branches', nargs='+', type=str, + help='target branches, e.g. specific release branches') +parser.add_argument( + '--verbose', default=False, action='store_true', + help='print out commit hashes and other detailed information' +) +parser.add_argument( + '--uplift', default=False, action='store_true', + help='uplift l10n commits missing from specified branches (if not specified, dry-run is performed)' +) +args = parser.parse_args() + +# remember the current branch, so that we can return to it once we're done. +current_branch = run_cmd_checked(["git rev-parse --abbrev-ref HEAD"]) + +try: + for branch in args.branches: + uplift_commits(branch, args.verbose, args.uplift) +finally: + # go back to the branch we were on before 'uplift_for_branches' ran + run_cmd_checked([f"git checkout {current_branch}"])