diff --git a/.taskcluster.yml b/.taskcluster.yml index d349fa519..b175ab5da 100644 --- a/.taskcluster.yml +++ b/.taskcluster.yml @@ -174,19 +174,12 @@ tasks: - index.mobile.v2.${project}.branch.${short_head_branch}.latest.taskgraph.decision - index.mobile.v2.${project}.branch.${short_head_branch}.revision.${head_sha}.taskgraph.decision - index.mobile.v2.${project}.revision.${head_sha}.taskgraph.decision - # TODO Bug 1631839: Remove the following routes once all consumers have migrated - - index.project.mobile.${project}.v2.branch.${short_head_branch}.latest.taskgraph.decision - - index.project.mobile.${project}.v2.branch.${short_head_branch}.revision.${head_sha}.taskgraph.decision - $if: 'tasks_for == "cron"' then: # cron context provides ${head_branch} as a short one - index.mobile.v2.${project}.branch.${head_branch}.latest.taskgraph.decision-${cron.job_name} - index.mobile.v2.${project}.branch.${head_branch}.revision.${head_sha}.taskgraph.decision-${cron.job_name} - index.mobile.v2.${project}.branch.${head_branch}.revision.${head_sha}.taskgraph.cron.${ownTaskId} - # TODO Bug 1631839: Remove the following routes once all consumers have migrated - - index.project.mobile.${project}.v2.branch.${head_branch}.latest.taskgraph.decision-${cron.job_name} - - index.project.mobile.${project}.v2.branch.${head_branch}.revision.${head_sha}.taskgraph.decision-${cron.job_name} - - index.project.mobile.${project}.v2.branch.${head_branch}.revision.${head_sha}.taskgraph.cron.${ownTaskId} scopes: $if: 'tasks_for == "github-push"' then: diff --git a/app/build.gradle b/app/build.gradle index ff6ef3d6a..ddf6ce535 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -503,6 +503,7 @@ dependencies { implementation Deps.androidx_lifecycle_viewmodel implementation Deps.androidx_core implementation Deps.androidx_core_ktx + implementation Deps.androidx_dynamic_animation implementation Deps.androidx_transition implementation Deps.androidx_work_ktx implementation Deps.google_material diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt index 9a9ced7e5..55c1dc3d6 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt @@ -85,10 +85,10 @@ private fun assertShowSearchSuggestions() { private fun assertShowSearchShortcuts() { onView(withId(androidx.preference.R.id.recycler_view)).perform( RecyclerViewActions.scrollTo( - hasDescendant(withText("Show search shortcuts")) + hasDescendant(withText("Show search engines")) ) ) - onView(withText("Show search shortcuts")) + onView(withText("Show search engines")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } @@ -146,11 +146,11 @@ private fun toggleShowSearchSuggestions() { private fun toggleShowSearchShortcuts() { onView(withId(androidx.preference.R.id.recycler_view)).perform( RecyclerViewActions.scrollTo( - hasDescendant(withText("Show search shortcuts")) + hasDescendant(withText("Show search engines")) ) ) - onView(withText("Show search shortcuts")) + onView(withText("Show search engines")) .perform(click()) } diff --git a/app/src/geckoBeta/java/org/mozilla/fenix/engine/GeckoProvider.kt b/app/src/geckoBeta/java/org/mozilla/fenix/engine/GeckoProvider.kt index 96aa0d767..b5171c98a 100644 --- a/app/src/geckoBeta/java/org/mozilla/fenix/engine/GeckoProvider.kt +++ b/app/src/geckoBeta/java/org/mozilla/fenix/engine/GeckoProvider.kt @@ -5,7 +5,9 @@ import android.content.Context import android.os.Bundle import mozilla.components.browser.engine.gecko.autofill.GeckoLoginDelegateWrapper +import mozilla.components.browser.engine.gecko.ext.toContentBlockingSetting import mozilla.components.browser.engine.gecko.glean.GeckoAdapter +import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy import mozilla.components.concept.storage.LoginsStorage import mozilla.components.lib.crash.handler.CrashHandlerService import mozilla.components.service.sync.logins.GeckoLoginStorageDelegate @@ -21,10 +23,11 @@ object GeckoProvider { @Synchronized fun getOrCreateRuntime( context: Context, - storage: Lazy + storage: Lazy, + trackingProtectionPolicy: TrackingProtectionPolicy ): GeckoRuntime { if (runtime == null) { - runtime = createRuntime(context, storage) + runtime = createRuntime(context, storage, trackingProtectionPolicy) } return runtime!! @@ -32,7 +35,8 @@ object GeckoProvider { private fun createRuntime( context: Context, - storage: Lazy + storage: Lazy, + policy: TrackingProtectionPolicy ): GeckoRuntime { val builder = GeckoRuntimeSettings.Builder() @@ -44,6 +48,7 @@ object GeckoProvider { val runtimeSettings = builder .crashHandler(CrashHandlerService::class.java) .telemetryDelegate(GeckoAdapter()) + .contentBlocking(policy.toContentBlockingSetting()) .aboutConfigEnabled(Config.channel.isBeta) .debugLogging(Config.channel.isDebug) .build() diff --git a/app/src/geckoNightly/java/org/mozilla/fenix/engine/GeckoProvider.kt b/app/src/geckoNightly/java/org/mozilla/fenix/engine/GeckoProvider.kt index 2508f6f57..43a2dd510 100644 --- a/app/src/geckoNightly/java/org/mozilla/fenix/engine/GeckoProvider.kt +++ b/app/src/geckoNightly/java/org/mozilla/fenix/engine/GeckoProvider.kt @@ -5,7 +5,9 @@ import android.content.Context import android.os.Bundle import mozilla.components.browser.engine.gecko.autofill.GeckoLoginDelegateWrapper +import mozilla.components.browser.engine.gecko.ext.toContentBlockingSetting import mozilla.components.browser.engine.gecko.glean.GeckoAdapter +import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy import mozilla.components.concept.storage.LoginsStorage import mozilla.components.lib.crash.handler.CrashHandlerService import mozilla.components.service.sync.logins.GeckoLoginStorageDelegate @@ -21,10 +23,11 @@ object GeckoProvider { @Synchronized fun getOrCreateRuntime( context: Context, - storage: Lazy + storage: Lazy, + trackingProtectionPolicy: TrackingProtectionPolicy ): GeckoRuntime { if (runtime == null) { - runtime = createRuntime(context, storage) + runtime = createRuntime(context, storage, trackingProtectionPolicy) } return runtime!! @@ -32,7 +35,8 @@ object GeckoProvider { private fun createRuntime( context: Context, - storage: Lazy + storage: Lazy, + policy: TrackingProtectionPolicy ): GeckoRuntime { val builder = GeckoRuntimeSettings.Builder() @@ -44,6 +48,7 @@ object GeckoProvider { val runtimeSettings = builder .crashHandler(CrashHandlerService::class.java) .telemetryDelegate(GeckoAdapter()) + .contentBlocking(policy.toContentBlockingSetting()) .debugLogging(Config.channel.isDebug) .aboutConfigEnabled(true) .build() diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index 2f76237ed..6e6d4a04d 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -24,4 +24,9 @@ object FeatureFlags { * Enables new tab tray pref */ val tabTray = Config.channel.isNightlyOrDebug + + /** + * Enables swipe on toolbar to switch tabs + */ + val swipeToSwitchTabs = Config.channel.isNightlyOrDebug } diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index b91e40fca..ec45fd058 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -139,14 +139,7 @@ open class FenixApplication : LocaleAwareApplication() { prefetchForHomeFragment() setupLeakCanary() - if (settings().isTelemetryEnabled) { - components.analytics.metrics.start(MetricServiceType.Data) - } - - if (settings().isMarketingTelemetryEnabled) { - components.analytics.metrics.start(MetricServiceType.Marketing) - } - + startMetricsIfEnabled() setupPush() visibilityLifecycleCallback = VisibilityLifecycleCallback(getSystemService()) @@ -158,49 +151,73 @@ open class FenixApplication : LocaleAwareApplication() { // runStorageMaintenance() // } - val taskQueue = components.performance.visualCompletenessQueue - registerActivityLifecycleCallbacks( - PerformanceActivityLifecycleCallbacks(taskQueue) - ) + initVisualCompletenessQueueAndQueueTasks() + } - // Enable the service-experiments component to be initialized after visual completeness - // for performance wins. - if (settings().isExperimentationEnabled) { - taskQueue.runIfReadyOrQueue { - Experiments.initialize( - applicationContext = applicationContext, - onExperimentsUpdated = { - ExperimentsManager.initSearchWidgetExperiment(this) - }, - configuration = mozilla.components.service.experiments.Configuration( - httpClient = components.core.client, - kintoEndpoint = KINTO_ENDPOINT_PROD - ) - ) - ExperimentsManager.initSearchWidgetExperiment(this) - } - } else { - // We should make a better way to opt out for when we have more experiments - // See https://github.com/mozilla-mobile/fenix/issues/6278 - ExperimentsManager.optOutSearchWidgetExperiment(this) + private fun initVisualCompletenessQueueAndQueueTasks() { + val taskQueue = components.performance.visualCompletenessQueue + + fun initQueue() { + registerActivityLifecycleCallbacks(PerformanceActivityLifecycleCallbacks(taskQueue)) } - components.performance.visualCompletenessQueue.runIfReadyOrQueue { - GlobalScope.launch(Dispatchers.IO) { - logger.info("Running post-visual completeness tasks...") - logElapsedTime(logger, "Storage initialization") { - components.core.historyStorage.warmUp() - components.core.bookmarksStorage.warmUp() - components.core.passwordsStorage.warmUp() - } - } - // Account manager initialization needs to happen on the main thread. - GlobalScope.launch(Dispatchers.Main) { - logElapsedTime(logger, "Kicking-off account manager") { - components.backgroundServices.accountManager + fun queueInitExperiments() { + if (settings().isExperimentationEnabled) { + taskQueue.runIfReadyOrQueue { + Experiments.initialize( + applicationContext = applicationContext, + onExperimentsUpdated = { + ExperimentsManager.initSearchWidgetExperiment(this) + }, + configuration = mozilla.components.service.experiments.Configuration( + httpClient = components.core.client, + kintoEndpoint = KINTO_ENDPOINT_PROD + ) + ) + ExperimentsManager.initSearchWidgetExperiment(this) + } + } else { + // We should make a better way to opt out for when we have more experiments + // See https://github.com/mozilla-mobile/fenix/issues/6278 + ExperimentsManager.optOutSearchWidgetExperiment(this) + } + } + + fun queueInitStorageAndServices() { + components.performance.visualCompletenessQueue.runIfReadyOrQueue { + GlobalScope.launch(Dispatchers.IO) { + logger.info("Running post-visual completeness tasks...") + logElapsedTime(logger, "Storage initialization") { + components.core.historyStorage.warmUp() + components.core.bookmarksStorage.warmUp() + components.core.passwordsStorage.warmUp() + } + } + // Account manager initialization needs to happen on the main thread. + GlobalScope.launch(Dispatchers.Main) { + logElapsedTime(logger, "Kicking-off account manager") { + components.backgroundServices.accountManager + } } } } + + initQueue() + + // We init these items in the visual completeness queue to avoid them initing in the critical + // startup path, before the UI finishes drawing (i.e. visual completeness). + queueInitExperiments() + queueInitStorageAndServices() + } + + private fun startMetricsIfEnabled() { + if (settings().isTelemetryEnabled) { + components.analytics.metrics.start(MetricServiceType.Data) + } + + if (settings().isMarketingTelemetryEnabled) { + components.analytics.metrics.start(MetricServiceType.Marketing) + } } // See https://github.com/mozilla-mobile/fenix/issues/7227 for context. @@ -231,6 +248,7 @@ open class FenixApplication : LocaleAwareApplication() { components.core.topSiteStorage.prefetch() } } + private fun setupPush() { // Sets the PushFeature as the singleton instance for push messages to go to. // We need the push feature setup here to deliver messages in the case where the service diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 0efa5e962..254898460 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -6,11 +6,14 @@ package org.mozilla.fenix import android.content.Context import android.content.Intent +import android.os.Build import android.os.Bundle import android.os.StrictMode import android.text.format.DateUtils import android.util.AttributeSet +import android.view.KeyEvent import android.view.View +import android.view.ViewConfiguration import android.view.WindowManager import androidx.annotation.CallSuper import androidx.annotation.IdRes @@ -30,6 +33,8 @@ import kotlinx.android.synthetic.main.activity_home.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.session.SessionManager @@ -52,6 +57,7 @@ import mozilla.components.support.ktx.android.content.share import mozilla.components.support.ktx.kotlin.isUrl import mozilla.components.support.ktx.kotlin.toNormalizedUrl import mozilla.components.support.locale.LocaleAwareAppCompatActivity +import mozilla.components.support.utils.RunWhenReadyQueue import mozilla.components.support.utils.SafeIntent import mozilla.components.support.utils.toSafeIntent import mozilla.components.support.webextensions.WebExtensionPopupFeature @@ -97,7 +103,6 @@ import org.mozilla.fenix.theme.DefaultThemeManager import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.trackingprotectionexceptions.TrackingProtectionExceptionsFragmentDirections import org.mozilla.fenix.utils.BrowsersCache -import org.mozilla.fenix.utils.RunWhenReadyQueue /** * The main activity of the application. The application is primarily a single Activity (this one) @@ -139,6 +144,9 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { ) } + // See onKeyDown for why this is necessary + private var backLongPressJob: Job? = null + private lateinit var navigationToolbar: Toolbar final override fun onCreate(savedInstanceState: Bundle?) { @@ -349,6 +357,50 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { super.onBackPressed() } + private fun isAndroidN(): Boolean = + Build.VERSION.SDK_INT == Build.VERSION_CODES.N || Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1 + + private fun handleBackLongPress(): Boolean { + supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach { + if (it is OnBackLongPressedListener && it.onBackLongPressed()) { + return true + } + } + return false + } + + final override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + // Inspired by https://searchfox.org/mozilla-esr68/source/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java#584-613 + // Android N has broken passing onKeyLongPress events for the back button, so we + // instead implement the long press behavior ourselves + // - For short presses, we cancel the callback in onKeyUp + // - For long presses, the normal keypress is marked as cancelled, hence won't be handled elsewhere + // (but Android still provides the haptic feedback), and the long press action is run + if (isAndroidN() && keyCode == KeyEvent.KEYCODE_BACK) { + backLongPressJob = lifecycleScope.launch { + delay(ViewConfiguration.getLongPressTimeout().toLong()) + handleBackLongPress() + } + } + return super.onKeyDown(keyCode, event) + } + + final override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { + if (isAndroidN() && keyCode == KeyEvent.KEYCODE_BACK) { + backLongPressJob?.cancel() + } + return super.onKeyUp(keyCode, event) + } + + final override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean { + // onKeyLongPress is broken in Android N so we don't handle back button long presses here + // for N. The version check ensures we don't handle back button long presses twice. + if (!isAndroidN() && keyCode == KeyEvent.KEYCODE_BACK) { + return handleBackLongPress() + } + return super.onKeyLongPress(keyCode, event) + } + final override fun onUserLeaveHint() { supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach { if (it is UserInteractionHandler && it.onHomePressed()) { diff --git a/app/src/main/java/org/mozilla/fenix/OnBackLongPressedListener.kt b/app/src/main/java/org/mozilla/fenix/OnBackLongPressedListener.kt new file mode 100644 index 000000000..e47a0b71b --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/OnBackLongPressedListener.kt @@ -0,0 +1,21 @@ +/* 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 + +/** + * Interface for features and fragments that want to handle long presses of the system back button + */ +interface OnBackLongPressedListener { + + /** + * Called when the system back button is long pressed. + * + * Note: This cannot be called when gesture navigation is enabled on Android 10+ due to system + * limitations. + * + * @return true if the event was handled + */ + fun onBackLongPressed(): Boolean +} 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 25be5f7b5..0c5387476 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -77,6 +77,7 @@ import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.NavGraphDirections +import org.mozilla.fenix.OnBackLongPressedListener import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.readermode.DefaultReaderModeController @@ -116,7 +117,8 @@ import java.lang.ref.WeakReference */ @ExperimentalCoroutinesApi @Suppress("TooManyFunctions", "LargeClass") -abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, SessionManager.Observer { +abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, SessionManager.Observer, + OnBackLongPressedListener { private lateinit var browserFragmentStore: BrowserFragmentStore private lateinit var browserAnimator: BrowserAnimator @@ -383,7 +385,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session tryAgain = downloadFeature::tryAgain, onCannotOpenFile = { FenixSnackbar.make( - view = view, + view = view.browserLayout, duration = Snackbar.LENGTH_SHORT, isDisplayedWithBrowserToolbar = true ) @@ -647,7 +649,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session val onCannotOpenFile = { FenixSnackbar.make( - view = view, + view = view.browserLayout, duration = Snackbar.LENGTH_SHORT, isDisplayedWithBrowserToolbar = true ) @@ -757,6 +759,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session removeSessionIfNeeded() } + override fun onBackLongPressed(): Boolean { + findNavController().navigate(R.id.action_global_tabHistoryDialogFragment) + return true + } + /** * Saves the external app session ID to be restored later in [onViewStateRestored]. */ @@ -925,7 +932,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session view?.let { view -> FenixSnackbar.make( - view = view, + view = view.browserLayout, duration = FenixSnackbar.LENGTH_LONG, isDisplayedWithBrowserToolbar = true ) @@ -971,7 +978,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session // Close find in page bar if opened findInPageIntegration.onBackPressed() FenixSnackbar.make( - view = requireView(), + view = requireView().browserLayout, duration = Snackbar.LENGTH_SHORT, isDisplayedWithBrowserToolbar = false ) 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 4c3b5882b..9c4f33728 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -15,6 +15,7 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.Snackbar +import kotlinx.android.synthetic.main.fragment_browser.* import kotlinx.android.synthetic.main.fragment_browser.view.* import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.browser.session.Session @@ -29,6 +30,7 @@ import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tabs.WindowFeature import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.addons.runIfFragmentIsAttached import org.mozilla.fenix.components.FenixSnackbar @@ -66,11 +68,24 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { return view } + @Suppress("LongMethod") override fun initializeUI(view: View): Session? { val context = requireContext() val components = context.components return super.initializeUI(view)?.also { + if (FeatureFlags.swipeToSwitchTabs) { + gestureLayout.addGestureListener( + ToolbarGestureHandler( + activity = requireActivity(), + contentLayout = browserLayout, + tabPreview = tabPreview, + toolbarLayout = browserToolbarView.view, + sessionManager = components.core.sessionManager + ) + ) + } + val readerModeAction = BrowserToolbar.ToggleButton( image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_readermode)!!, @@ -243,7 +258,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { } } FenixSnackbar.make( - view = view, + view = view.browserLayout, duration = Snackbar.LENGTH_SHORT, isDisplayedWithBrowserToolbar = true ) diff --git a/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt b/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt new file mode 100644 index 000000000..c1bccab6c --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt @@ -0,0 +1,136 @@ +/* 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.browser + +import android.content.Context +import android.graphics.PointF +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.MotionEvent +import android.widget.FrameLayout +import androidx.core.view.GestureDetectorCompat + +/** + * Interface that allows intercepting and handling swipe gestures received in a [SwipeGestureLayout]. + */ +interface SwipeGestureListener { + + /** + * Called when the [SwipeGestureLayout] detects the start of a swipe gesture. The listener + * should return true if it wants to handle the swipe gesture. If the listener returns false + * it will not receive any callbacks for future events that the swipe produces. + * + * @param start the initial point where the gesture started + * @param next the next point in the gesture + */ + fun onSwipeStarted(start: PointF, next: PointF): Boolean + + /** + * Called when the swipe gesture receives a new event. + * + * @param distanceX the change along the x-axis since the last swipe update + * @param distanceY the change along the y-axis since the last swipe update + */ + fun onSwipeUpdate(distanceX: Float, distanceY: Float) + + /** + * Called when the user finishes the swipe gesture (ie lifts their finger off the screen) + * + * @param velocityX the velocity of the swipe along the x-axis + * @param velocityY the velocity of the swipe along the y-axis + */ + fun onSwipeFinished(velocityX: Float, velocityY: Float) +} + +/** + * A [FrameLayout] that allows listeners to intercept and handle swipe events. + * + * Listeners are called in the order they are added and the first listener to intercept a swipe event + * is the only listener that will receive events for the duration of that swipe. + */ +class SwipeGestureLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + private val gestureListener = object : GestureDetector.SimpleOnGestureListener() { + override fun onDown(e: MotionEvent?): Boolean { + return true + } + + override fun onScroll( + e1: MotionEvent?, + e2: MotionEvent?, + distanceX: Float, + distanceY: Float + ): Boolean { + val start = e1?.let { event -> PointF(event.rawX, event.rawY) } ?: return false + val next = e2?.let { event -> PointF(event.rawX, event.rawY) } ?: return false + + if (activeListener == null && !handledInitialScroll) { + activeListener = listeners.firstOrNull { listener -> + listener.onSwipeStarted(start, next) + } + handledInitialScroll = true + } + activeListener?.onSwipeUpdate(distanceX, distanceY) + return activeListener != null + } + + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent?, + velocityX: Float, + velocityY: Float + ): Boolean { + activeListener?.onSwipeFinished(velocityX, velocityY) + return if (activeListener != null) { + activeListener = null + true + } else { + false + } + } + } + + private val gestureDetector = GestureDetectorCompat(context, gestureListener) + + private val listeners = mutableListOf() + private var activeListener: SwipeGestureListener? = null + private var handledInitialScroll = false + + fun addGestureListener(listener: SwipeGestureListener) { + listeners.add(listener) + } + + override fun onInterceptTouchEvent(event: MotionEvent?): Boolean { + return when (event?.actionMasked) { + MotionEvent.ACTION_DOWN -> { + handledInitialScroll = false + gestureDetector.onTouchEvent(event) + false + } + else -> gestureDetector.onTouchEvent(event) + } + } + + override fun onTouchEvent(event: MotionEvent?): Boolean { + return when (event?.actionMasked) { + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { + gestureDetector.onTouchEvent(event) + // If the active listener is not null here, then we haven't detected a fling + // so notify the listener that the swipe was finished with 0 velocity + activeListener?.onSwipeFinished( + velocityX = 0f, + velocityY = 0f + ) + activeListener = null + false + } + else -> gestureDetector.onTouchEvent(event) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/browser/TabPreview.kt b/app/src/main/java/org/mozilla/fenix/browser/TabPreview.kt new file mode 100644 index 000000000..ba90b09bb --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/browser/TabPreview.kt @@ -0,0 +1,70 @@ +/* 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.browser + +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.doOnNextLayout +import androidx.core.view.updateLayoutParams +import kotlinx.android.synthetic.main.mozac_ui_tabcounter_layout.view.* +import kotlinx.android.synthetic.main.tab_preview.view.* +import mozilla.components.browser.thumbnails.loader.ThumbnailLoader +import mozilla.components.support.images.ImageLoadRequest +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.theme.ThemeManager +import kotlin.math.max + +class TabPreview @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : FrameLayout(context, attrs, defStyle) { + + private val thumbnailLoader = ThumbnailLoader(context.components.core.thumbnailStorage) + + init { + val inflater = LayoutInflater.from(context) + inflater.inflate(R.layout.tab_preview, this, true) + + if (!context.settings().shouldUseBottomToolbar) { + fakeToolbar.updateLayoutParams { + gravity = Gravity.TOP + } + + fakeToolbar.background = ResourcesCompat.getDrawable( + resources, + ThemeManager.resolveAttribute(R.attr.bottomBarBackgroundTop, context), + null + ) + } + + // Change view properties to avoid confusing the UI tests + tab_button.counter_box.id = View.NO_ID + tab_button.counter_text.id = View.NO_ID + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + previewThumbnail.translationY = if (!context.settings().shouldUseBottomToolbar) { + fakeToolbar.height.toFloat() + } else { + 0f + } + } + + fun loadPreviewThumbnail(thumbnailId: String) { + doOnNextLayout { + val thumbnailSize = max(previewThumbnail.height, previewThumbnail.width) + thumbnailLoader.loadIntoView(previewThumbnail, ImageLoadRequest(thumbnailId, thumbnailSize)) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt b/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt new file mode 100644 index 000000000..00508690d --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt @@ -0,0 +1,356 @@ +/* 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.browser + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.app.Activity +import android.graphics.PointF +import android.graphics.Rect +import android.os.Build +import android.util.TypedValue +import android.view.View +import android.view.ViewConfiguration +import androidx.annotation.Dimension +import androidx.annotation.Dimension.DP +import androidx.core.graphics.contains +import androidx.core.graphics.toPoint +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isVisible +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.FlingAnimation +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.SessionManager +import mozilla.components.support.ktx.android.util.dpToPx +import mozilla.components.support.ktx.android.view.getRectWithViewLocation +import org.mozilla.fenix.ext.sessionsOfType +import org.mozilla.fenix.ext.settings +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +/** + * Handles intercepting touch events on the toolbar for swipe gestures and executes the + * necessary animations. + */ +@Suppress("LargeClass", "TooManyFunctions") +class ToolbarGestureHandler( + private val activity: Activity, + private val contentLayout: View, + private val tabPreview: TabPreview, + private val toolbarLayout: View, + private val sessionManager: SessionManager +) : SwipeGestureListener { + + private enum class GestureDirection { + LEFT_TO_RIGHT, RIGHT_TO_LEFT + } + + private sealed class Destination { + data class Tab(val session: Session) : Destination() + object None : Destination() + } + + private val windowWidth: Int + get() = activity.resources.displayMetrics.widthPixels + + private val windowInsets: WindowInsetsCompat? + get() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // In theory, the rootWindowInsets should exist at this point but if the decorView is + // not attached for some reason we'll get a NullPointerException without the check. + activity.window.decorView.rootWindowInsets?.let { + WindowInsetsCompat.toWindowInsetsCompat(it) + } + } else { + null + } + + private val previewOffset = PREVIEW_OFFSET.dpToPx(activity.resources.displayMetrics) + + private val touchSlop = ViewConfiguration.get(activity).scaledTouchSlop + private val minimumFlingVelocity = ViewConfiguration.get(activity).scaledMinimumFlingVelocity + private val defaultVelocity = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + MINIMUM_ANIMATION_VELOCITY, + activity.resources.displayMetrics + ) + + private var gestureDirection = GestureDirection.LEFT_TO_RIGHT + + override fun onSwipeStarted(start: PointF, next: PointF): Boolean { + val dx = next.x - start.x + val dy = next.y - start.y + gestureDirection = if (dx < 0) { + GestureDirection.RIGHT_TO_LEFT + } else { + GestureDirection.LEFT_TO_RIGHT + } + + return if (start.isInToolbar() && abs(dx) > touchSlop && abs(dy) < abs(dx)) { + preparePreview(getDestination()) + true + } else { + false + } + } + + override fun onSwipeUpdate(distanceX: Float, distanceY: Float) { + when (getDestination()) { + is Destination.Tab -> { + // Restrict the range of motion for the views so you can't start a swipe in one direction + // then move your finger far enough in the other direction and make the content visually + // start sliding off screen the other way. + tabPreview.translationX = when (gestureDirection) { + GestureDirection.RIGHT_TO_LEFT -> min( + windowWidth.toFloat() + previewOffset, + tabPreview.translationX - distanceX + ) + GestureDirection.LEFT_TO_RIGHT -> max( + -windowWidth.toFloat() - previewOffset, + tabPreview.translationX - distanceX + ) + } + contentLayout.translationX = when (gestureDirection) { + GestureDirection.RIGHT_TO_LEFT -> min( + 0f, + contentLayout.translationX - distanceX + ) + GestureDirection.LEFT_TO_RIGHT -> max( + 0f, + contentLayout.translationX - distanceX + ) + } + } + is Destination.None -> { + // If there is no "next" tab to swipe to in the gesture direction, only do a + // partial animation to show that we are at the end of the tab list + val maxContentHidden = contentLayout.width * OVERSCROLL_HIDE_PERCENT + contentLayout.translationX = when (gestureDirection) { + GestureDirection.RIGHT_TO_LEFT -> max( + -maxContentHidden.toFloat(), + contentLayout.translationX - distanceX + ).coerceAtMost(0f) + GestureDirection.LEFT_TO_RIGHT -> min( + maxContentHidden.toFloat(), + contentLayout.translationX - distanceX + ).coerceAtLeast(0f) + } + } + } + } + + override fun onSwipeFinished( + velocityX: Float, + velocityY: Float + ) { + val destination = getDestination() + if (destination is Destination.Tab && isGestureComplete(velocityX)) { + animateToNextTab(velocityX, destination.session) + } else { + animateCanceledGesture(velocityX) + } + } + + private fun createFlingAnimation( + view: View, + minValue: Float, + maxValue: Float, + startVelocity: Float + ): FlingAnimation = + FlingAnimation(view, DynamicAnimation.TRANSLATION_X).apply { + setMinValue(minValue) + setMaxValue(maxValue) + setStartVelocity(startVelocity) + friction = ViewConfiguration.getScrollFriction() + } + + private fun getDestination(): Destination { + val isLtr = activity.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR + val currentSession = sessionManager.selectedSession ?: return Destination.None + val currentIndex = sessionManager.sessionsOfType(currentSession.private).indexOfFirst { + it.id == currentSession.id + } + + return if (currentIndex == -1) { + Destination.None + } else { + val sessions = sessionManager.sessionsOfType(currentSession.private) + val index = when (gestureDirection) { + GestureDirection.RIGHT_TO_LEFT -> if (isLtr) { + currentIndex + 1 + } else { + currentIndex - 1 + } + GestureDirection.LEFT_TO_RIGHT -> if (isLtr) { + currentIndex - 1 + } else { + currentIndex + 1 + } + } + + if (index < sessions.count() && index >= 0) { + Destination.Tab(sessions.elementAt(index)) + } else { + Destination.None + } + } + } + + private fun preparePreview(destination: Destination) { + val thumbnailId = when (destination) { + is Destination.Tab -> destination.session.id + is Destination.None -> return + } + + tabPreview.loadPreviewThumbnail(thumbnailId) + tabPreview.alpha = 1f + tabPreview.translationX = when (gestureDirection) { + GestureDirection.RIGHT_TO_LEFT -> windowWidth.toFloat() + previewOffset + GestureDirection.LEFT_TO_RIGHT -> -windowWidth.toFloat() - previewOffset + } + tabPreview.isVisible = true + } + + /** + * Checks if the gesture is complete based on the position of tab preview and the velocity of + * the gesture. A completed gesture means the user has indicated they want to swipe to the next + * tab. The gesture is considered complete if one of the following is true: + * + * 1. The user initiated a fling in the same direction as the initial movement + * 2. There is no fling initiated, but the percentage of the tab preview shown is at least + * [GESTURE_FINISH_PERCENT] + * + * If the user initiated a fling in the opposite direction of the initial movement, the + * gesture is always considered incomplete. + */ + private fun isGestureComplete(velocityX: Float): Boolean { + val previewWidth = tabPreview.getRectWithViewLocation().visibleWidth.toDouble() + val velocityMatchesDirection = when (gestureDirection) { + GestureDirection.RIGHT_TO_LEFT -> velocityX <= 0 + GestureDirection.LEFT_TO_RIGHT -> velocityX >= 0 + } + val reverseFling = + abs(velocityX) >= minimumFlingVelocity && !velocityMatchesDirection + + return !reverseFling && (previewWidth / windowWidth >= GESTURE_FINISH_PERCENT || + abs(velocityX) >= minimumFlingVelocity) + } + + private fun getVelocityFromFling(velocityX: Float): Float { + return max(abs(velocityX), defaultVelocity) + } + + private fun animateToNextTab(velocityX: Float, session: Session) { + val browserFinalXCoordinate: Float = when (gestureDirection) { + GestureDirection.RIGHT_TO_LEFT -> -windowWidth.toFloat() - previewOffset + GestureDirection.LEFT_TO_RIGHT -> windowWidth.toFloat() + previewOffset + } + val animationVelocity = when (gestureDirection) { + GestureDirection.RIGHT_TO_LEFT -> -getVelocityFromFling(velocityX) + GestureDirection.LEFT_TO_RIGHT -> getVelocityFromFling(velocityX) + } + + // Finish animating the contentLayout off screen and tabPreview on screen + createFlingAnimation( + view = contentLayout, + minValue = min(0f, browserFinalXCoordinate), + maxValue = max(0f, browserFinalXCoordinate), + startVelocity = animationVelocity + ).addUpdateListener { _, value, _ -> + tabPreview.translationX = when (gestureDirection) { + GestureDirection.RIGHT_TO_LEFT -> value + windowWidth + previewOffset + GestureDirection.LEFT_TO_RIGHT -> value - windowWidth - previewOffset + } + }.addEndListener { _, _, _, _ -> + contentLayout.translationX = 0f + sessionManager.select(session) + + // Fade out the tab preview to prevent flickering + val shortAnimationDuration = + activity.resources.getInteger(android.R.integer.config_shortAnimTime) + tabPreview.animate() + .alpha(0f) + .setDuration(shortAnimationDuration.toLong()) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + tabPreview.isVisible = false + } + }) + }.start() + } + + private fun animateCanceledGesture(gestureVelocity: Float) { + val velocity = if (getDestination() is Destination.None) { + defaultVelocity + } else { + getVelocityFromFling(gestureVelocity) + }.let { v -> + when (gestureDirection) { + GestureDirection.RIGHT_TO_LEFT -> v + GestureDirection.LEFT_TO_RIGHT -> -v + } + } + + createFlingAnimation( + view = contentLayout, + minValue = min(0f, contentLayout.translationX), + maxValue = max(0f, contentLayout.translationX), + startVelocity = velocity + ).addUpdateListener { _, value, _ -> + tabPreview.translationX = when (gestureDirection) { + GestureDirection.RIGHT_TO_LEFT -> value + windowWidth + previewOffset + GestureDirection.LEFT_TO_RIGHT -> value - windowWidth - previewOffset + } + }.addEndListener { _, _, _, _ -> + tabPreview.isVisible = false + }.start() + } + + private fun PointF.isInToolbar(): Boolean { + val toolbarLocation = toolbarLayout.getRectWithViewLocation() + // In Android 10, the system gesture touch area overlaps the bottom of the toolbar, so + // lets make our swipe area taller by that amount + windowInsets?.let { insets -> + if (activity.settings().shouldUseBottomToolbar) { + toolbarLocation.top -= (insets.mandatorySystemGestureInsets.bottom - insets.stableInsetBottom) + } + } + return toolbarLocation.contains(toPoint()) + } + + private val Rect.visibleWidth: Int + get() = if (left < 0) { + right + } else { + windowWidth - left + } + + companion object { + /** + * The percentage of the tab preview that needs to be visible to consider the + * tab switching gesture complete. + */ + private const val GESTURE_FINISH_PERCENT = 0.25 + + /** + * The percentage of the content view that can be hidden by the tab switching gesture if + * there is not tab available to switch to + */ + private const val OVERSCROLL_HIDE_PERCENT = 0.20 + + /** + * The speed of the fling animation (in dp per second). + */ + @Dimension(unit = DP) + private const val MINIMUM_ANIMATION_VELOCITY = 1500f + + /** + * The size of the gap between the tab preview and content layout. + */ + @Dimension(unit = DP) + private const val PREVIEW_OFFSET = 48 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt index feb74728c..4ac0145d9 100644 --- a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt +++ b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt @@ -30,6 +30,7 @@ import mozilla.components.service.fxa.manager.SCOPE_SYNC import mozilla.components.service.fxa.manager.SyncEnginesStorage import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider import mozilla.components.service.sync.logins.SyncableLoginsStorage +import mozilla.components.support.utils.RunWhenReadyQueue import org.mozilla.fenix.Config import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R @@ -39,7 +40,6 @@ import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.sync.SyncedTabsIntegration import org.mozilla.fenix.utils.Mockable -import org.mozilla.fenix.utils.RunWhenReadyQueue import org.mozilla.fenix.utils.Settings /** 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 d3ec37d3a..5574844f1 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -86,7 +86,11 @@ class Core(private val context: Context) { GeckoEngine( context, defaultSettings, - GeckoProvider.getOrCreateRuntime(context, lazyPasswordsStorage) + GeckoProvider.getOrCreateRuntime( + context, + lazyPasswordsStorage, + trackingProtectionPolicyFactory.createTrackingProtectionPolicy() + ) ).also { WebCompatFeature.install(it) @@ -108,7 +112,11 @@ class Core(private val context: Context) { val client: Client by lazy { GeckoViewFetchClient( context, - GeckoProvider.getOrCreateRuntime(context, lazyPasswordsStorage) + GeckoProvider.getOrCreateRuntime( + context, + lazyPasswordsStorage, + trackingProtectionPolicyFactory.createTrackingProtectionPolicy() + ) ) } diff --git a/app/src/main/java/org/mozilla/fenix/components/PerformanceComponent.kt b/app/src/main/java/org/mozilla/fenix/components/PerformanceComponent.kt index 88f03aa40..615d348a8 100644 --- a/app/src/main/java/org/mozilla/fenix/components/PerformanceComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/components/PerformanceComponent.kt @@ -4,7 +4,7 @@ package org.mozilla.fenix.components -import org.mozilla.fenix.utils.RunWhenReadyQueue +import mozilla.components.support.utils.RunWhenReadyQueue /** * Component group for all functionality related to performance. 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 41795a446..565224752 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 @@ -170,7 +170,13 @@ class DefaultBrowserToolbarController( Do exhaustive when (item) { ToolbarMenu.Item.Back -> sessionUseCases.goBack.invoke(currentSession) - ToolbarMenu.Item.Forward -> sessionUseCases.goForward.invoke(currentSession) + is ToolbarMenu.Item.Forward -> { + if (item.viewHistory) { + navController.navigate(R.id.action_global_tabHistoryDialogFragment) + } else { + sessionUseCases.goForward.invoke(currentSession) + } + } ToolbarMenu.Item.Reload -> sessionUseCases.reload.invoke(currentSession) ToolbarMenu.Item.Stop -> sessionUseCases.stopLoading.invoke(currentSession) ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically { @@ -333,7 +339,7 @@ class DefaultBrowserToolbarController( private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) { val eventItem = when (item) { ToolbarMenu.Item.Back -> Event.BrowserMenuItemTapped.Item.BACK - ToolbarMenu.Item.Forward -> Event.BrowserMenuItemTapped.Item.FORWARD + is ToolbarMenu.Item.Forward -> Event.BrowserMenuItemTapped.Item.FORWARD ToolbarMenu.Item.Reload -> Event.BrowserMenuItemTapped.Item.RELOAD ToolbarMenu.Item.Stop -> Event.BrowserMenuItemTapped.Item.STOP ToolbarMenu.Item.Settings -> Event.BrowserMenuItemTapped.Item.SETTINGS diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt index a27691bac..caf922208 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt @@ -82,9 +82,10 @@ class DefaultToolbarMenu( session?.canGoForward ?: true }, secondaryImageTintResource = ThemeManager.resolveAttribute(R.attr.disabled, context), - disableInSecondaryState = true + disableInSecondaryState = true, + longClickListener = { onItemTapped.invoke(ToolbarMenu.Item.Forward(viewHistory = true)) } ) { - onItemTapped.invoke(ToolbarMenu.Item.Forward) + onItemTapped.invoke(ToolbarMenu.Item.Forward(viewHistory = false)) } val refresh = BrowserMenuItemToolbar.TwoStateButton( diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt index db8dbbe14..aeb33dd15 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt @@ -14,7 +14,7 @@ interface ToolbarMenu { object FindInPage : Item() object Share : Item() object Back : Item() - object Forward : Item() + data class Forward(val viewHistory: Boolean) : Item() object Reload : Item() object Stop : Item() object OpenInFenix : Item() diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt index 32a33b8d4..a5f0a9017 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt @@ -75,9 +75,10 @@ class CustomTabToolbarMenu( R.attr.disabled, context ), - disableInSecondaryState = true + disableInSecondaryState = true, + longClickListener = { onItemTapped.invoke(ToolbarMenu.Item.Forward(viewHistory = true)) } ) { - onItemTapped.invoke(ToolbarMenu.Item.Forward) + onItemTapped.invoke(ToolbarMenu.Item.Forward(viewHistory = false)) } val refresh = BrowserMenuItemToolbar.TwoStateButton( diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt index 3cfc8f25b..1172d0192 100644 --- a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt @@ -119,10 +119,10 @@ class AwesomeBarView( bookmarksStorageSuggestionProvider = BookmarksStorageSuggestionProvider( - components.core.bookmarksStorage, - loadUrlUseCase, - components.core.icons, - engineForSpeculativeConnects + bookmarksStorage = components.core.bookmarksStorage, + loadUrlUseCase = loadUrlUseCase, + icons = components.core.icons, + engine = engineForSpeculativeConnects ) val searchBitmap = getDrawable(context, R.drawable.ic_search)!!.apply { diff --git a/app/src/main/java/org/mozilla/fenix/session/PerformanceActivityLifecycleCallbacks.kt b/app/src/main/java/org/mozilla/fenix/session/PerformanceActivityLifecycleCallbacks.kt index 63a14cf5e..793a222cb 100644 --- a/app/src/main/java/org/mozilla/fenix/session/PerformanceActivityLifecycleCallbacks.kt +++ b/app/src/main/java/org/mozilla/fenix/session/PerformanceActivityLifecycleCallbacks.kt @@ -7,11 +7,11 @@ package org.mozilla.fenix.session import android.app.Activity import android.app.Application import android.os.Bundle +import mozilla.components.support.utils.RunWhenReadyQueue import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.browser.BrowserPerformanceTestActivity import org.mozilla.fenix.settings.account.AuthIntentReceiverActivity -import org.mozilla.fenix.utils.RunWhenReadyQueue import org.mozilla.fenix.widget.VoiceSearchActivity /** diff --git a/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt index 363bf901c..7bda04ec7 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt @@ -10,13 +10,11 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast import androidx.core.content.pm.PackageInfoCompat import androidx.fragment.app.Fragment import androidx.recyclerview.widget.DividerItemDecoration import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import kotlinx.android.synthetic.main.fragment_about.* -import kotlinx.coroutines.ExperimentalCoroutinesApi import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.HomeActivity @@ -39,10 +37,9 @@ import org.mozilla.geckoview.BuildConfig as GeckoViewBuildConfig * Displays the logo and information about the app, including library versions. */ class AboutFragment : Fragment(), AboutPageListener { + private lateinit var appName: String private val aboutPageAdapter: AboutPageAdapter = AboutPageAdapter(this) - private var secretDebugMenuClicks = 0 - private var lastDebugMenuToast: Toast? = null override fun onCreateView( inflater: LayoutInflater, @@ -56,15 +53,7 @@ class AboutFragment : Fragment(), AboutPageListener { return rootView } - override fun onResume() { - super.onResume() - secretDebugMenuClicks = 0 - } - - @ExperimentalCoroutinesApi override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - about_list.run { adapter = aboutPageAdapter addItemDecoration( @@ -75,33 +64,10 @@ class AboutFragment : Fragment(), AboutPageListener { ) } - // 5 taps on the logo activate the "secret" debug menu. - wordmark.setOnClickListener { - // Because the user will mostly likely tap the logo in rapid succession, - // we ensure only 1 toast is shown at any given time. - lastDebugMenuToast?.let { toast -> toast.cancel() } - secretDebugMenuClicks += 1 - when (secretDebugMenuClicks) { - in 2 until SECRET_DEBUG_MENU_CLICKS -> { - val clicksLeft = SECRET_DEBUG_MENU_CLICKS - secretDebugMenuClicks - val toast = Toast.makeText( - context, - getString(R.string.about_debug_menu_toast_progress, clicksLeft), - Toast.LENGTH_SHORT - ) - toast.show() - lastDebugMenuToast = toast - } - SECRET_DEBUG_MENU_CLICKS -> { - Toast.makeText( - context, - getString(R.string.about_debug_menu_toast_done), - Toast.LENGTH_LONG - ).show() - requireContext().settings().showSecretDebugMenuThisSession = true - } - } - } + lifecycle.addObserver(SecretDebugMenuTrigger( + logoView = wordmark, + settings = view.context.settings() + )) populateAboutHeader() aboutPageAdapter.submitList(populateAboutList()) @@ -233,7 +199,5 @@ class AboutFragment : Fragment(), AboutPageListener { companion object { private const val ABOUT_LICENSE_URL = "about:license" - // Number of clicks on the app logo to enable the "secret" debug menu. - private const val SECRET_DEBUG_MENU_CLICKS = 5 } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/about/SecretDebugMenuTrigger.kt b/app/src/main/java/org/mozilla/fenix/settings/about/SecretDebugMenuTrigger.kt new file mode 100644 index 000000000..75f42943b --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/about/SecretDebugMenuTrigger.kt @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.about + +import android.view.View +import android.widget.Toast +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import org.mozilla.fenix.R +import org.mozilla.fenix.utils.Settings + +/** + * Triggers the "secret" debug menu when logoView is tapped 5 times. + */ +class SecretDebugMenuTrigger( + logoView: View, + private val settings: Settings +) : View.OnClickListener, LifecycleObserver { + + private var secretDebugMenuClicks = 0 + private var lastDebugMenuToast: Toast? = null + + init { + if (!settings.showSecretDebugMenuThisSession) { + logoView.setOnClickListener(this) + } + } + + /** + * Reset the [secretDebugMenuClicks] counter. + */ + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun clearClickCounter() { + secretDebugMenuClicks = 0 + } + + override fun onClick(v: View) { + // Because the user will mostly likely tap the logo in rapid succession, + // we ensure only 1 toast is shown at any given time. + lastDebugMenuToast?.cancel() + secretDebugMenuClicks += 1 + when (secretDebugMenuClicks) { + in 2 until SECRET_DEBUG_MENU_CLICKS -> { + val clicksLeft = SECRET_DEBUG_MENU_CLICKS - secretDebugMenuClicks + val toast = Toast.makeText( + v.context, + v.context.getString(R.string.about_debug_menu_toast_progress, clicksLeft), + Toast.LENGTH_SHORT + ) + toast.show() + lastDebugMenuToast = toast + } + SECRET_DEBUG_MENU_CLICKS -> { + Toast.makeText( + v.context, + R.string.about_debug_menu_toast_done, + Toast.LENGTH_LONG + ).show() + settings.showSecretDebugMenuThisSession = true + } + } + } + + companion object { + // Number of clicks on the app logo to enable the "secret" debug menu. + private const val SECRET_DEBUG_MENU_CLICKS = 5 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsFragment.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsFragment.kt index 8f27a2cb9..4059d5159 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsFragment.kt @@ -37,6 +37,7 @@ class SyncedTabsFragment : LibraryPageFragment() { syncedTabsFeature.set( feature = SyncedTabsFeature( + context = requireContext(), storage = backgroundServices.syncedTabsStorage, accountManager = backgroundServices.accountManager, view = synced_tabs_layout, diff --git a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryAdapter.kt new file mode 100644 index 000000000..433c620e1 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryAdapter.kt @@ -0,0 +1,40 @@ +/* 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.tabhistory + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.fenix.R + +data class TabHistoryItem( + val title: String, + val url: String, + val index: Int, + val isSelected: Boolean +) + +class TabHistoryAdapter( + private val interactor: TabHistoryViewInteractor +) : RecyclerView.Adapter() { + + var historyList: List = emptyList() + set(value) { + field = value + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabHistoryViewHolder { + val view = + LayoutInflater.from(parent.context).inflate(R.layout.history_list_item, parent, false) + return TabHistoryViewHolder(view, interactor) + } + + override fun onBindViewHolder(holder: TabHistoryViewHolder, position: Int) { + holder.bind(historyList[position]) + } + + override fun getItemCount(): Int = historyList.size +} diff --git a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryController.kt b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryController.kt new file mode 100644 index 000000000..2d6ecc784 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryController.kt @@ -0,0 +1,24 @@ +/* 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.tabhistory + +import androidx.navigation.NavController +import mozilla.components.feature.session.SessionUseCases +import org.mozilla.fenix.R + +interface TabHistoryController { + fun handleGoToHistoryItem(item: TabHistoryItem) +} + +class DefaultTabHistoryController( + private val navController: NavController, + private val goToHistoryIndexUseCase: SessionUseCases.GoToHistoryIndexUseCase +) : TabHistoryController { + + override fun handleGoToHistoryItem(item: TabHistoryItem) { + navController.popBackStack(R.id.browserFragment, false) + goToHistoryIndexUseCase.invoke(item.index) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryDialogFragment.kt new file mode 100644 index 000000000..5ae534b73 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryDialogFragment.kt @@ -0,0 +1,56 @@ +/* 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.tabhistory + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kotlinx.android.synthetic.main.fragment_tab_history_dialog.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import mozilla.components.lib.state.ext.consumeFrom +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.requireComponents + +class TabHistoryDialogFragment : BottomSheetDialogFragment() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, R.style.BottomSheet) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_tab_history_dialog, container, false) + + @ExperimentalCoroutinesApi + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val controller = DefaultTabHistoryController( + navController = findNavController(), + goToHistoryIndexUseCase = requireComponents.useCases.sessionUseCases.goToHistoryIndex + ) + val tabHistoryView = TabHistoryView( + container = tabHistoryLayout, + expandDialog = ::expand, + interactor = TabHistoryInteractor(controller) + ) + + consumeFrom(requireComponents.core.store) { + tabHistoryView.updateState(it) + } + } + + private fun expand() { + (dialog as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryInteractor.kt new file mode 100644 index 000000000..1c659acf0 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryInteractor.kt @@ -0,0 +1,14 @@ +/* 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.tabhistory + +class TabHistoryInteractor( + private val controller: TabHistoryController +) : TabHistoryViewInteractor { + + override fun goToHistoryItem(item: TabHistoryItem) { + controller.handleGoToHistoryItem(item) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryView.kt b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryView.kt new file mode 100644 index 000000000..c02ba038d --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryView.kt @@ -0,0 +1,79 @@ +/* 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.tabhistory + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.component_tabhistory.* +import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.state.BrowserState +import org.mozilla.fenix.R + +interface TabHistoryViewInteractor { + + /** + * Jump to a specific index in the tab's history. + */ + fun goToHistoryItem(item: TabHistoryItem) +} + +class TabHistoryView( + private val container: ViewGroup, + private val expandDialog: () -> Unit, + interactor: TabHistoryViewInteractor +) : LayoutContainer { + + override val containerView: View? + get() = container + + val view: View = LayoutInflater.from(container.context) + .inflate(R.layout.component_tabhistory, container, true) + + private val adapter = TabHistoryAdapter(interactor) + private val layoutManager = object : LinearLayoutManager(view.context) { + override fun onLayoutCompleted(state: RecyclerView.State?) { + super.onLayoutCompleted(state) + currentIndex?.let { index -> + // Force expansion of the dialog, otherwise scrolling to the current history item + // won't work when its position is near the bottom of the recyclerview. + expandDialog.invoke() + // Also, attempt to center the current history item. + val itemView = tabHistoryRecyclerView.findViewHolderForLayoutPosition( + findFirstCompletelyVisibleItemPosition() + )?.itemView + val offset = tabHistoryRecyclerView.height / 2 - (itemView?.height ?: 0) / 2 + scrollToPositionWithOffset(index, offset) + } + } + }.apply { + reverseLayout = true + } + + private var currentIndex: Int? = null + + init { + tabHistoryRecyclerView.adapter = adapter + tabHistoryRecyclerView.layoutManager = layoutManager + } + + fun updateState(state: BrowserState) { + state.selectedTab?.content?.history?.let { historyState -> + currentIndex = historyState.currentIndex + val items = historyState.items.mapIndexed { index, historyItem -> + TabHistoryItem( + title = historyItem.title, + url = historyItem.uri, + index = index, + isSelected = index == historyState.currentIndex + ) + } + adapter.historyList = items + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolder.kt new file mode 100644 index 000000000..88bd26d55 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryViewHolder.kt @@ -0,0 +1,34 @@ +/* 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.tabhistory + +import android.view.View +import androidx.core.text.bold +import androidx.core.text.buildSpannedString +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.history_list_item.view.* + +class TabHistoryViewHolder( + private val view: View, + private val interactor: TabHistoryViewInteractor +) : RecyclerView.ViewHolder(view) { + + fun bind(item: TabHistoryItem) { + view.history_layout.overflowView.isVisible = false + view.history_layout.urlView.text = item.url + view.history_layout.loadFavicon(item.url) + + view.history_layout.titleView.text = if (item.isSelected) { + buildSpannedString { + bold { append(item.title) } + } + } else { + item.title + } + + view.setOnClickListener { interactor.goToHistoryItem(item) } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/utils/RunWhenReadyQueue.kt b/app/src/main/java/org/mozilla/fenix/utils/RunWhenReadyQueue.kt deleted file mode 100644 index 9b7167d1c..000000000 --- a/app/src/main/java/org/mozilla/fenix/utils/RunWhenReadyQueue.kt +++ /dev/null @@ -1,57 +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.utils - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.util.concurrent.CopyOnWriteArrayList -import java.util.concurrent.atomic.AtomicBoolean - -/** - * A queue that acts as a gate, either executing tasks right away if the queue is marked as "ready", - * i.e. gate is open, or queues them to be executed whenever the queue is marked as ready in the - * future, i.e. gate becomes open. - */ -class RunWhenReadyQueue { - private val tasks = CopyOnWriteArrayList<() -> Unit>() - private val isReady = AtomicBoolean(false) - - /** - * Was this queue ever marked as 'ready' via a call to [ready]? - * - * @return Boolean value indicating if this queue is 'ready'. - */ - fun isReady(): Boolean = isReady.get() - - /** - * Runs the [task] if this queue is marked as ready, or queues it for later execution. - * Task will be executed on the main thread. - * - * @param task: The task to run now if queue is ready or queue for later execution. - */ - fun runIfReadyOrQueue(task: () -> Unit) { - if (isReady.get()) { - CoroutineScope(Dispatchers.Main).launch { task.invoke() } - } else { - tasks.add(task) - } - } - - /** - * Mark queue as ready. Pending tasks will execute, and all tasks passed to [runIfReadyOrQueue] - * after this point will be executed immediately. - */ - fun ready() { - // Make sure that calls to `ready` are idempotent. - if (!isReady.compareAndSet(false, true)) { - return - } - - CoroutineScope(Dispatchers.Main).launch { - tasks.forEach { it.invoke() }.also { tasks.clear() } - } - } -} diff --git a/app/src/main/res/layout/component_tabhistory.xml b/app/src/main/res/layout/component_tabhistory.xml new file mode 100644 index 000000000..3095029a2 --- /dev/null +++ b/app/src/main/res/layout/component_tabhistory.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_browser.xml b/app/src/main/res/layout/fragment_browser.xml index 08dc10d49..63e9fde8d 100644 --- a/app/src/main/res/layout/fragment_browser.xml +++ b/app/src/main/res/layout/fragment_browser.xml @@ -2,51 +2,66 @@ - - + + + + + + + + + + + + + + + + - - - - - - - - - - + diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index c2a77fa70..ebdc0bc21 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -147,7 +147,7 @@ android:layout_marginTop="@dimen/search_fragment_shortcuts_label_margin_vertical" android:layout_marginEnd="@dimen/search_fragment_shortcuts_label_margin_horizontal" android:visibility="gone" - android:text="@string/search_shortcuts_search_with_2" + android:text="@string/search_engines_search_with" app:layout_constraintStart_toStartOf="@id/scrollable_area" app:layout_constraintTop_toBottomOf="@id/awesomeBar_barrier" tools:text="This time, search with:" /> @@ -197,8 +197,8 @@ \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_tab_history_dialog.xml b/app/src/main/res/layout/fragment_tab_history_dialog.xml new file mode 100644 index 000000000..3be7d859a --- /dev/null +++ b/app/src/main/res/layout/fragment_tab_history_dialog.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout/tab_preview.xml b/app/src/main/res/layout/tab_preview.xml new file mode 100644 index 000000000..e7da4ce04 --- /dev/null +++ b/app/src/main/res/layout/tab_preview.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 9daebae5a..d543ac084 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -102,6 +102,9 @@ + + diff --git a/app/src/main/res/values-en-rCA/strings.xml b/app/src/main/res/values-en-rCA/strings.xml index 704aef3df..29a9b6b6c 100644 --- a/app/src/main/res/values-en-rCA/strings.xml +++ b/app/src/main/res/values-en-rCA/strings.xml @@ -573,6 +573,8 @@ Select folder Are you sure you want to delete this folder? + + %s will delete the selected items. Deleted %1$s @@ -627,8 +629,10 @@ Deleted %1$s - + Bookmarks deleted + + Deleting selected folders UNDO @@ -721,6 +725,8 @@ %d tab selected Tabs saved! + + Collection saved! Tab saved! @@ -825,6 +831,10 @@ DENY Are you sure you want to delete %1$s? + + Deleting this tab will delete the entire collection. You can create new collections at any time. + + Delete %1$s? Delete @@ -879,8 +889,6 @@ Automatically deletes browsing data when you select "Quit" from the main menu Automatically deletes browsing data when you select \"Quit\" from the main menu - - Browsing history Quit @@ -1223,6 +1231,8 @@ Logins and passwords that are not saved will be shown here. Logins and passwords will not be saved for these sites. + + Delete all exceptions Search logins @@ -1261,6 +1271,8 @@ Copy username Copy site + + Open site in browser Show password diff --git a/app/src/main/res/values-es-rAR/strings.xml b/app/src/main/res/values-es-rAR/strings.xml index c2d6cefbb..4f578f553 100644 --- a/app/src/main/res/values-es-rAR/strings.xml +++ b/app/src/main/res/values-es-rAR/strings.xml @@ -593,6 +593,8 @@ Seleccionar carpeta ¿Estás seguro de que querés eliminar eliminar esta carpeta? + + %s va a eliminar los elementos seleccionados. Se eliminó %1$s @@ -650,8 +652,10 @@ The first parameter is the host part of the URL of the bookmark deleted, if any --> Se eliminó %1$s - + Marcadores eliminados + + Eliminar carpetas seleccionadas UNDO @@ -746,6 +750,8 @@ ¡Pestañas guardadas! + + ¡Colección guardada! ¡Pestaña guardada! @@ -852,6 +858,10 @@ DENY ¿Estás seguro de que querés eliminar %1$s? + + Eliminar esta pestaña va a eliminar toda la colección. Podés crear nuevas colecciones en cualquier momento. + + ¿Eliminar %1$s? Eliminar @@ -1249,6 +1259,8 @@ Los inicios de sesión y las contraseñas que no se guardan se mostrarán aquí. Los inicios de sesión y las contraseñas no se van a guardar para estos sitios. + + Eliminar todas las excepciones Buscar inicios de sesión @@ -1287,6 +1299,8 @@ Copiar nombre de usuario Copiar sitio + + Abrir sitio en el navegador Mostrar contraseña diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index efd81663e..34b10de40 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -581,6 +581,8 @@ Válasszon mappát Biztos, hogy törölni szeretné ezt a mappát? + + A(z) %s törölni fogja a kiválasztott elemeket. %1$s törölve @@ -636,8 +638,10 @@ %1$s törölve - + Könyvjelzők törölve + + Kiválasztott mappák törlése VISSZAVONÁS @@ -731,6 +735,8 @@ %d lap kiválasztva Lapok mentve. + + Gyűjtemény mentve. Lap mentve. @@ -838,6 +844,10 @@ ELUTASÍTÁS Biztos, hogy törli ezt: %1$s? + + A lap törlésével törli az egész gyűjteményt. Bármikor létrehozhat új gyűjteményeket. + + Törli ezt: %1$s? Törlés @@ -893,8 +903,6 @@ Automatikusan törli a böngészési adatokat, ha a főmenüben a „Kilépés” lehetőséget választja - - Böngészési előzmények Kilépés @@ -1244,6 +1252,8 @@ Itt jelennek meg a nem mentett bejelentkezések és jelszavak. A bejelentkezéseket és a jelszavak nem lesznek elmentve ezeknél a webhelyeknél. + + Összes kivétel törlése Bejelentkezések keresése @@ -1282,6 +1292,8 @@ Felhasználónév másolása Oldal másolása + + Oldal megnyitása böngészőben Jelszó megjelenítése diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml index f62480695..6cc73e343 100644 --- a/app/src/main/res/values-kk/strings.xml +++ b/app/src/main/res/values-kk/strings.xml @@ -569,6 +569,8 @@ Буманы таңдау Бұл буманы өшіруді шынымен қалайсыз ба? + + %s таңдалған элементтерді өшіреді. %1$s өшірілді @@ -623,8 +625,10 @@ %1$s өшірілді - + Бетбелгілер өшірілді + + Таңдалған бумаларды өшіру БОЛДЫРМАУ @@ -718,6 +722,8 @@ %d бет таңдалды Беттер сақталды! + + Жинақ сақталды! Бет сақталды! @@ -822,6 +828,10 @@ ТЫЙЫМ САЛУ %1$s өшіруді шынымен қалайсыз ба? + + Бұл бетті өшіру жинақты толығымен өшіреді. Жаңа жинақтарды кез келген уақытта жасауға болады. + + %1$s өшіру керек пе? Өшіру @@ -878,8 +888,6 @@ Негізгі мәзірден "Шығу" таңдау кезінде, шолу деректерін автоматты түрде өшіреді Негізгі мәзірден \"Шығу\" таңдау кезінде, шолу деректерін автоматты түрде өшіреді - - Шолу тарихы Шығу @@ -1227,6 +1235,8 @@ Сақталмаған логиндер мен парольдер осында көрсетіледі. Бұл сайттар үшін логиндер мен парольдер сақталмайды. + + Барлық ережеден тыс жағдайларды өшіру Логиндерден іздеу @@ -1265,6 +1275,8 @@ Пайдаланушы атын көшіріп алу Сайтты көшіріп алу + + Сайтты браузерде ашу Парольді көрсету diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index fac8150c4..e5804fd6a 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -593,6 +593,8 @@ 폴더 선택 이 폴더를 삭제하시겠습니까? + + %s가 선택된 항목들을 삭제합니다. %1$s 삭제됨 @@ -652,8 +654,10 @@ %1$s 삭제됨 - + 북마크 삭제됨 + + 선택한 폴더 삭제 중 실행 취소 @@ -757,6 +761,8 @@ 탭이 저장되었습니다! + + 모음집 저장됨! 탭이 저장되었습니다! @@ -869,6 +875,10 @@ 거부 %1$s 파일을 삭제하시겠습니까? + + 이 탭을 삭제하면 전체 모음집이 삭제됩니다. 언제든지 새 모음집을 만들 수 있습니다. + + %1$s 모음집을 삭제하시겠습니까? 삭제 @@ -1276,6 +1286,8 @@ 저장되지 않은 로그인과 비밀번호가 여기에 표시됩니다. 이 사이트에 대한 로그인과 비밀번호는 저장되지 않습니다. + + 모든 예외 삭제 로그인 검색 @@ -1314,6 +1326,8 @@ 사용자 이름 복사 사이트 복사 + + 브라우저에서 사이트 열기 비밀번호 보이기 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 9d85866e9..51452ce7b 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -582,6 +582,8 @@ Ar tikrai norite pašalinti šį aplanką? + + „%s“ pašalins pasirinktus elementus. Pašalintas „%1$s“ @@ -637,8 +639,10 @@ The first parameter is the host part of the URL of the bookmark deleted, if any --> Pašalintas %1$s - + Adresyno įrašai pašalinti + + Šalinami pasirinkti aplankai Atšaukti @@ -732,6 +736,8 @@ Pažymėta %d kortelė Kortelės įrašytos! + + Rinkinys įrašytas! Kortelė įrašyta! @@ -837,6 +843,10 @@ Drausti Ar tikrai norite pašalinti „%1$s“? + + Pašalindami šią kortelę, pašalinsite visą rinkinį. Naujus rinkinius galite sukurti bet kada. + + Pašalinti „%1$s“? Pašalinti @@ -894,8 +904,6 @@ Naršymo duomenys bus pašalinami automatiškai, pagrindiniame meniu pasirinkus „Išeiti“ Naršymo duomenys bus pašalinami automatiškai, pagrindiniame meniu pasirinkus „Išeiti“ - - Naršymo žurnalas Išeiti @@ -1281,6 +1289,8 @@ Kopijuoti naudotojo vardą Kopijuoti svetainę + + Atverti svetainę naršyklėje Rodyti slaptažodį diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 7157f81b0..7fa5cc3d8 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -582,6 +582,8 @@ Velg mappe Er du sikker på at du vil slette denne mappen? + + %s vil slette de valgte elementene. Slettet %1$s @@ -637,8 +639,10 @@ The first parameter is the host part of the URL of the bookmark deleted, if any --> Slettet %1$s - + Bokmerker slettet + + Sletter valgte mapper ANGRE @@ -734,6 +738,8 @@ Faner lagret! + + Samling lagret! Fane lagret! @@ -838,6 +844,10 @@ AVSLÅ Er du sikker på at du vil slette %1$s? + + Hvis du sletter denne fanen, blir hele samlingen slettet. Du kan når som helst lage nye samlinger. + + Vil du slette %1$s? Slett @@ -894,8 +904,6 @@ Sletter nettleserdata automatisk når du velger «Avslutt» fra hovedmenyen Sletter nettleserdata automatisk når du velger «Avslutt» fra hovedmenyen - - Nettleserhistorikk Avslutt @@ -1250,6 +1258,8 @@ Innlogginger og passord som ikke er lagret vil vises her. Innlogginger og passord vil ikke bli lagret for disse nettstedene. + + Slett alle unntak Søk innlogginger @@ -1289,6 +1299,8 @@ Kopier brukernavn Kopier nettsted + + Åpne nettsted i nettleseren Vis passord diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 1c91ae40e..da3638ceb 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -584,6 +584,8 @@ Map selecteren Weet u zeker dat u deze map wilt verwijderen? + + %s zal de geselecteerde items verwijderen. %1$s verwijderd @@ -638,8 +640,10 @@ %1$s verwijderd - + Bladwijzers verwijderd + + Geselecteerde mappen verwijderen ONGEDAAN MAKEN @@ -733,6 +737,8 @@ %d tabblad geselecteerd Tabbladen opgeslagen! + + Collectie opgeslagen! Tabblad opgeslagen! @@ -840,6 +846,10 @@ WEIGEREN Weet u zeker dat u %1$s wilt verwijderen? + + Als u dit tabblad verwijdert, wordt de hele collectie verwijderd. U kunt op elk moment nieuwe collecties maken. + + %1$s verwijderen? Verwijderen @@ -895,8 +905,6 @@ Verwijdert automatisch navigatiegegevens wanneer u in het hoofdmenu ‘Afsluiten’ selecteert Verwijdert automatisch navigatiegegevens wanneer u in het hoofdmenu ‘Afsluiten’ selecteert - - Navigatiegeschiedenis Afsluiten @@ -1243,6 +1251,8 @@ Niet-opgeslagen aanmeldingen en wachtwoorden worden hier weergegeven. Aanmeldingen en wachtwoorden worden voor deze websites niet opgeslagen. + + Alle uitzonderingen verwijderen Aanmeldingen zoeken @@ -1281,6 +1291,8 @@ Gebruikersnaam kopiëren Website kopiëren + + Website openen in browser Wachtwoord tonen diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 063da93c1..a85a6e3c3 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -576,6 +576,8 @@ Selecionar pasta Tem certeza que deseja excluir esta pasta? + + O %s excluirá os itens selecionados. %1$s excluída @@ -630,8 +632,10 @@ Excluiu %1$s - + Favoritos excluídos + + Excluindo pastas selecionadas DESFAZER @@ -726,6 +730,8 @@ %d aba selecionada Abas salvas! + + Coleção salva! Aba salva! @@ -830,6 +836,10 @@ NEGAR Tem certeza que deseja excluir %1$s? + + Excluir esta aba também excluirá toda a coleção. Você pode criar novas coleções quando quiser. + + Excluir %1$s? Excluir @@ -1234,6 +1244,8 @@ Contas e senhas que não são salvas são mostradas aqui. Contas e senhas desses sites não serão salvas. + + Excluir todas as exceções Pesquisar contas @@ -1272,6 +1284,8 @@ Copiar nome de usuário Copiar site + + Abrir site no navegador Exibir senha diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 7e477b53e..9304c25b2 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -586,6 +586,8 @@ Välj mapp Är du säker på att du vill ta bort den här mappen? + + %s kommer att radera de markerade objekten. Tog bort %1$s @@ -640,8 +642,10 @@ %1$s har tagits bort - + Bokmärken borttagna + + Tar bort valda mappar ÅNGRA @@ -735,6 +739,8 @@ %d flik vald Flikar sparade! + + Samling sparad! Flik sparad! @@ -843,6 +849,10 @@ NEKA Är du säker att du vill ta bort %1$s? + + Om du tar bort den här fliken raderas hela samlingen. Du kan skapa nya samlingar när som helst. + + Tog bort %1$s? Ta bort @@ -899,8 +909,6 @@ Tar automatiskt bort surfdata när du väljer "Avsluta" från huvudmenyn Tar automatiskt bort surfdata när du väljer \"Avsluta\" från huvudmenyn - - Webbläsarhistorik Avsluta @@ -1249,6 +1257,8 @@ Inloggningar och lösenord som inte sparas visas här. Inloggningar och lösenord sparas inte för dessa webbplatser. + + Ta bort alla undantag Sök inloggningar @@ -1287,6 +1297,8 @@ Kopiera användarnamn Kopiera webbplats + + Öppna webbplatsen i webbläsaren Visa lösenord diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 3bc20595b..13a458b21 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -117,7 +117,7 @@ %1$s ile aç - %1$s TARAFINDAN GELİŞTİRİLDİ + %1$s SEKMESİ diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 5d2534f8c..65fdc0699 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -575,6 +575,8 @@ Chọn thư mục Bạn có chắc chắn muốn xóa thư mục này? + + %s sẽ xóa các mục đã chọn. Đã xóa %1$s @@ -629,8 +631,10 @@ Đã xóa %1$s - + Đã xóa dấu trang + + Đang xóa các thư mục đã chọn HOÀN TÁC @@ -724,6 +728,8 @@ %d thẻ được chọn Đã lưu các thẻ! + + Đã lưu bộ sưu tập! Đã lưu thẻ! @@ -828,6 +834,10 @@ TỪ CHỐI Bạn có chắc chắn muốn xóa %1$s không? + + Xóa thẻ này sẽ xóa bộ sưu tập này. Bạn có thể tạo bộ sưu tập mới bất cứ lúc nào. + + Xóa %1$s? Xóa @@ -1224,6 +1234,8 @@ Đăng nhập và mật khẩu không được lưu sẽ được hiển thị ở đây. Đăng nhập và mật khẩu sẽ không được lưu cho các trang web này. + + Xóa tất cả các ngoại lệ Tìm thông tin đăng nhập @@ -1262,6 +1274,8 @@ Sao chép tên người dùng Sao chép URL trang web + + Mở trang web trong trình duyệt Hiện mật khẩu diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 919c11e20..d1e4e6f10 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -593,6 +593,8 @@ 选择文件夹 您确定要删除这个文件夹吗? + + %s 将删除所选项目。 已删除 %1$s @@ -650,8 +652,10 @@ 已删除 %1$s 条书签 - + 书签已删除 + + 正在删除所选文件夹 撤销 @@ -756,6 +760,8 @@ 标签页已保存! + + 收藏集已保存! 标签页已保存! @@ -867,6 +873,10 @@ 拒绝 您确定要删除“%1$s”吗? + + 删除此标签页将删除整个收藏集。您可以随时新建收藏集。 + + 要删除 %1$s 吗? 删除 @@ -1268,6 +1278,8 @@ 不保存登录名和密码的网站将显示于此处。 将不保存这些网站的登录名和密码。 + + 删除所有例外 搜索登录信息 @@ -1306,6 +1318,8 @@ 复制用户名 复制网站 + + 在浏览器中打开网站 显示密码 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index fc4c5b7a3..001d531d5 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -588,6 +588,8 @@ 選擇資料夾 您確定要刪除這個資料夾嗎? + + %s 將刪除選擇的項目。 已刪除 %1$s @@ -643,8 +645,10 @@ 已刪除書籤 %1$s - + 已刪除書籤 + + 刪除選擇的資料夾 還原 @@ -748,6 +752,8 @@ 已儲存分頁! + + 已儲存收藏集! 已儲存分頁! @@ -859,6 +865,10 @@ 拒絕 您確定要刪除 %1$s 嗎? + + 刪除此分頁也會同時刪除整個收藏集,您可以之後再建立新的收藏集。 + + 要刪除 %1$s 嗎? 刪除 @@ -1258,6 +1268,8 @@ 不儲存登入資訊與密碼的網站將顯示於此處。 將不儲存這些網站的登入資訊與密碼。 + + 刪除所有例外 搜尋登入資訊 @@ -1296,6 +1308,8 @@ 複製使用者名稱 複製網站 + + 用瀏覽器開啟 顯示密碼 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 33d219f38..51601631d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -146,13 +146,11 @@ Scan - Shortcuts + Search Engine Search engine settings - - Search with - This time, search with: + This time, search with: Fill link from clipboard @@ -258,8 +256,8 @@ Developer tools Remote debugging via USB - - Show search shortcuts + + Show search engines Show search suggestions @@ -1432,4 +1430,14 @@ To add a new top site, remove one. Long press the site and select remove. OK, Got It + + + + Shortcuts + + Search with + + This time, search with: + + Show search shortcuts diff --git a/app/src/main/res/xml/search_preferences.xml b/app/src/main/res/xml/search_preferences.xml index be7c2604e..5c6d3939b 100644 --- a/app/src/main/res/xml/search_preferences.xml +++ b/app/src/main/res/xml/search_preferences.xml @@ -27,7 +27,7 @@ + android:title="@string/preferences_show_search_engines" /> + + @Before + fun setup() { + MockKAnnotations.init(this) + mockkStatic(Toast::class) + clickListener = slot() + + every { logoView.setOnClickListener(capture(clickListener)) } just Runs + every { logoView.context } returns context + every { + context.getString(R.string.about_debug_menu_toast_progress, any()) + } returns "Debug menu: x click(s) left to enable" + every { settings.showSecretDebugMenuThisSession } returns false + every { settings.showSecretDebugMenuThisSession = any() } just Runs + every { Toast.makeText(context, any(), any()) } returns toast + every { Toast.makeText(context, any(), any()) } returns toast + } + + @After + fun teardown() { + unmockkStatic(Toast::class) + } + + @Test + fun `toast is not displayed on first click`() { + SecretDebugMenuTrigger(logoView, settings) + clickListener.captured.onClick(logoView) + + verify(inverse = true) { Toast.makeText(context, any(), any()) } + verify(inverse = true) { toast.show() } + } + + @Test + fun `toast is displayed on second click`() { + SecretDebugMenuTrigger(logoView, settings) + clickListener.captured.onClick(logoView) + clickListener.captured.onClick(logoView) + + verify { context.getString(R.string.about_debug_menu_toast_progress, 3) } + verify { Toast.makeText(context, any(), Toast.LENGTH_SHORT) } + verify { toast.show() } + } + + @Test + fun `clearClickCounter resets counter`() { + val trigger = SecretDebugMenuTrigger(logoView, settings) + + clickListener.captured.onClick(logoView) + trigger.clearClickCounter() + + clickListener.captured.onClick(logoView) + + verify(inverse = true) { Toast.makeText(context, any(), any()) } + verify(inverse = true) { toast.show() } + } + + @Test + fun `toast is displayed on fifth click`() { + SecretDebugMenuTrigger(logoView, settings) + clickListener.captured.onClick(logoView) + clickListener.captured.onClick(logoView) + clickListener.captured.onClick(logoView) + clickListener.captured.onClick(logoView) + clickListener.captured.onClick(logoView) + + verify { Toast.makeText( + context, + R.string.about_debug_menu_toast_done, + Toast.LENGTH_LONG + ) } + verify { toast.show() } + verify { settings.showSecretDebugMenuThisSession = true } + } + + @Test + fun `don't register click listener if menu is already shown`() { + every { settings.showSecretDebugMenuThisSession } returns true + SecretDebugMenuTrigger(logoView, settings) + + verify(inverse = true) { logoView.setOnClickListener(any()) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryControllerTest.kt b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryControllerTest.kt new file mode 100644 index 000000000..8b32cc10f --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryControllerTest.kt @@ -0,0 +1,38 @@ +/* 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.tabhistory + +import androidx.navigation.NavController +import io.mockk.mockk +import io.mockk.verify +import mozilla.components.browser.session.SessionManager +import mozilla.components.feature.session.SessionUseCases +import org.junit.Test + +class TabHistoryControllerTest { + + val sessionManager: SessionManager = mockk(relaxed = true) + val navController: NavController = mockk(relaxed = true) + val sessionUseCases = SessionUseCases(sessionManager) + val goToHistoryIndexUseCase = sessionUseCases.goToHistoryIndex + val controller = DefaultTabHistoryController( + navController = navController, + goToHistoryIndexUseCase = goToHistoryIndexUseCase + ) + + val currentItem = TabHistoryItem( + index = 0, + title = "", + url = "", + isSelected = true + ) + + @Test + fun handleGoToHistoryIndex() { + controller.handleGoToHistoryItem(currentItem) + + verify { goToHistoryIndexUseCase.invoke(currentItem.index) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryInteractorTest.kt new file mode 100644 index 000000000..d9ffc5ca2 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryInteractorTest.kt @@ -0,0 +1,24 @@ +/* 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.tabhistory + +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test + +class TabHistoryInteractorTest { + + val controller: TabHistoryController = mockk(relaxed = true) + val interactor = TabHistoryInteractor(controller) + + @Test + fun onGoToHistoryItem() { + val item: TabHistoryItem = mockk() + + interactor.goToHistoryItem(item) + + verify { controller.handleGoToHistoryItem(item) } + } +} diff --git a/buildSrc/src/main/java/AndroidComponents.kt b/buildSrc/src/main/java/AndroidComponents.kt index 71516a6cd..448860de0 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 = "51.0.20200721130108" + const val VERSION = "52.0.20200722023149" } diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 4421c1b0a..060e72ecf 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -28,6 +28,7 @@ object Versions { const val androidx_paging = "2.1.0" const val androidx_transition = "1.3.0" const val androidx_work = "2.2.0" + const val androidx_dynamic_animation = "1.0.0" const val google_material = "1.1.0" const val google_flexbox = "2.0.1" @@ -170,6 +171,7 @@ object Deps { const val androidx_recyclerview = "androidx.recyclerview:recyclerview:${Versions.androidx_recyclerview}" const val androidx_core = "androidx.core:core:${Versions.androidx_core}" const val androidx_core_ktx = "androidx.core:core-ktx:${Versions.androidx_core}" + const val androidx_dynamic_animation = "androidx.dynamicanimation:dynamicanimation:${Versions.androidx_dynamic_animation}" const val androidx_transition = "androidx.transition:transition:${Versions.androidx_transition}" const val androidx_work_ktx = "androidx.work:work-runtime-ktx:${Versions.androidx_work}" const val androidx_work_testing = "androidx.work:work-testing:${Versions.androidx_work}" diff --git a/taskcluster/fenix_taskgraph/routes.py b/taskcluster/fenix_taskgraph/routes.py index 6f7d03beb..32892295e 100644 --- a/taskcluster/fenix_taskgraph/routes.py +++ b/taskcluster/fenix_taskgraph/routes.py @@ -16,11 +16,6 @@ SIGNING_ROUTE_TEMPLATES = [ "index.{trust-domain}.v2.{project}.{variant}.{build_date}.revision.{head_rev}.{abi}", "index.{trust-domain}.v2.{project}.{variant}.{build_date}.latest.{abi}", "index.{trust-domain}.v2.{project}.{variant}.revision.{head_rev}.{abi}", - - # TODO Bug 1631839: Remove the following scopes once all consumers have migrated - "index.project.{trust-domain}.{project}.v2.{variant}.{build_date}.revision.{head_rev}", - "index.project.{trust-domain}.{project}.v2.{variant}.{build_date}.latest", - "index.project.{trust-domain}.{project}.v2.{variant}.latest", ]