1
0
Fork 0

Copione merged onto master

master
blallo 2020-08-20 00:00:20 +02:00
commit bd1d5c94be
69 changed files with 1935 additions and 457 deletions

View File

@ -105,21 +105,23 @@ you want these variants to be:
#### Performance Build Variants
For accurate performance measurements, read this section!
If you want to analyze performance during **local development** (note: there is a non-trivial performance impact - see caveats):
- Recommendation: use a debuggable variant (see "local.properties helpers" below) with local Leanplum, Adjust, & Sentry API tokens: contact the front-end perf group for access to them
- Rationale: There are numerous performance-impacting differences between debug and release variants so we need a release variant. To profile, we also need debuggable, which is disabled by default for release variants. If API tokens are not provided, the SDKs may change their behavior in non-trivial ways.
- Caveats:
- debuggable has a non-trivial & variable impact on performance but is needed to take profiles.
- Random experiment opt-in & feature flags may impact performance (see [perf-frontend-issues#45](https://github.com/mozilla-mobile/perf-frontend-issues/issues/45) for mitigation).
- This is slower to build than debug builds because it does additional tasks (e.g. minification) similar to other release builds
To analyze performance during **local development** build a production variant locally (this could either be the Nightly, beta or release). Otherwise, you could also grab a pre-existing APK if you don't need to test some local changes. Then, use the Firefox profiler to profile what you need!
If you want to run **performance tests/benchmarks** in automation or locally:
- Recommendation: production builds. If debuggable is required, use recommendation above but note the caveat above. If your needs are not met, please contact the front-end perf group to identify a new solution.
- Rationale: like the rationale above, we need release variants so the choice is based on the debuggable flag.
For more information on how to use the profiler or how to use the build, refer to this [how to measure performance with the build](https://wiki.mozilla.org/Performance/How_to_get_started_on_Fenix)
For additional context on these recommendations, see [the perf build variant analysis](https://docs.google.com/document/d/1aW-m0HYncTDDiRz_2x6EjcYkjBpL9SHhhYix13Vil30/edit#).
If you want to run **performance tests/benchmarks** in automation or locally use a production build since it is much closer in behavior compared to what users see in the wild.
Before you can install any release variants, **you will need to sign them:** see [Automatically signing release builds](#automatically-sign-release-builds) for details.
Before you can install any release builds, **You will need to sign production build variants:** see [Automatically signing release builds](#automatically-sign-release-builds) for details.
##### Known disabled-by-default features
Some features are disabled by default when Fenix is built locally. This can be problematic at times for checking performance since you might want to know how your code behaves with those features.
The known features that are disabled by default are:
- Sentry
- Leanplum
- Adjust
- Mozilla Location Services (also known as MLS)
- Firebase Push Services
- Telemetry (only disabled by default in debug builds)
## Pre-push hooks
To reduce review turn-around time, we'd like all pushes to run tests locally. We'd

View File

@ -5,7 +5,9 @@
package org.mozilla.fenix.helpers
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.UiDevice
import org.mozilla.fenix.HomeActivity
/**
@ -16,7 +18,12 @@ import org.mozilla.fenix.HomeActivity
*/
class HomeActivityTestRule(initialTouchMode: Boolean = false, launchActivity: Boolean = true) :
ActivityTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity)
ActivityTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity) {
override fun beforeActivityLaunched() {
super.beforeActivityLaunched()
setLongTapTimeout()
}
}
/**
* A [org.junit.Rule] to handle shared test set up for tests on [HomeActivity]. This adds
@ -26,5 +33,19 @@ class HomeActivityTestRule(initialTouchMode: Boolean = false, launchActivity: Bo
* @param launchActivity See [IntentsTestRule]
*/
class HomeActivityIntentTestRule(initialTouchMode: Boolean = false, launchActivity: Boolean = true) :
IntentsTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity)
class HomeActivityIntentTestRule(
initialTouchMode: Boolean = false,
launchActivity: Boolean = true
) :
IntentsTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity) {
override fun beforeActivityLaunched() {
super.beforeActivityLaunched()
setLongTapTimeout()
}
}
// changing the device preference for Touch and Hold delay, to avoid long-clicks instead of a single-click
fun setLongTapTimeout() {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.executeShellCommand("settings put secure long_press_timeout 3000")
}

View File

@ -10,7 +10,6 @@ import okhttp3.mockwebserver.MockWebServer
import org.junit.Rule
import org.junit.Before
import org.junit.After
import org.junit.Ignore
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
@ -69,7 +68,7 @@ class SettingsAboutTest {
}
}
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/13219")
@Test
fun verifyAboutFirefoxPreview() {
homeScreen {

View File

@ -222,7 +222,7 @@ private fun assertLibrariesUsed() {
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.perform(click())
onView(withId(R.id.action_bar)).check(matches(hasDescendant(withText(containsString("Firefox Preview | OSS Libraries")))))
onView(withId(R.id.toolbar)).check(matches(hasDescendant(withText(containsString("Firefox Preview | OSS Libraries")))))
Espresso.pressBack()
}

View File

@ -31,5 +31,6 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromEditCustomSearchEngineFragment(R.id.editCustomSearchEngineFragment),
FromAddonDetailsFragment(R.id.addonDetailsFragment),
FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment),
FromLoginDetailFragment(R.id.loginDetailFragment)
FromLoginDetailFragment(R.id.loginDetailFragment),
FromTabTray(R.id.tabTrayDialogFragment)
}

View File

@ -20,14 +20,11 @@ object FeatureFlags {
const val loginsEdit = true
/**
* Enable tab sync feature
* Shows Synced Tabs in the tabs tray.
*
* Tracking issue: https://github.com/mozilla-mobile/fenix/issues/13892
*/
const val syncedTabs = true
/**
* Enables new tab tray pref
*/
val tabTray = Config.channel.isNightlyOrDebug
val syncedTabsInTabsTray = Config.channel.isNightlyOrDebug
/**
* Enables viewing tab history
@ -48,4 +45,9 @@ object FeatureFlags {
* Enables downloads with external download managers.
*/
val externalDownloadManager = Config.channel.isNightlyOrDebug
/**
* Enables viewing downloads in browser.
*/
val viewDownloads = Config.channel.isNightlyOrDebug
}

View File

@ -100,6 +100,7 @@ import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirection
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.BrowsersCache
@ -597,6 +598,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromLoginDetailFragment ->
LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabTray ->
TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
}
/**

View File

@ -32,7 +32,6 @@ 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
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
@ -85,11 +84,8 @@ class BackgroundServices(
)
@VisibleForTesting
val supportedEngines = if (FeatureFlags.syncedTabs) {
val supportedEngines =
setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords, SyncEngine.Tabs)
} else {
setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords)
}
private val syncConfig = SyncConfig(supportedEngines, syncPeriodInMinutes = 240L) // four hours
init {
@ -98,10 +94,7 @@ class BackgroundServices(
GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage)
GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarkStorage)
GlobalSyncableStoreProvider.configureStore(SyncEngine.Passwords to passwordsStorage)
if (FeatureFlags.syncedTabs) {
GlobalSyncableStoreProvider.configureStore(SyncEngine.Tabs to remoteTabsStorage)
}
GlobalSyncableStoreProvider.configureStore(SyncEngine.Tabs to remoteTabsStorage)
}
private val telemetryAccountObserver = TelemetryAccountObserver(

View File

@ -488,7 +488,7 @@ sealed class Event {
NEW_PRIVATE_TAB, SHARE, BACK, FORWARD, RELOAD, STOP, OPEN_IN_FENIX,
SAVE_TO_COLLECTION, ADD_TO_TOP_SITES, ADD_TO_HOMESCREEN, QUIT, READER_MODE_ON,
READER_MODE_OFF, OPEN_IN_APP, BOOKMARK, READER_MODE_APPEARANCE, ADDONS_MANAGER,
BOOKMARKS, HISTORY, SYNC_TABS
BOOKMARKS, HISTORY, SYNC_TABS, DOWNLOADS
}
override val extras: Map<Events.browserMenuActionKeys, String>?

View File

@ -380,6 +380,13 @@ class DefaultBrowserToolbarController(
BrowserFragmentDirections.actionGlobalHistoryFragment()
)
}
ToolbarMenu.Item.Downloads -> browserAnimator.captureEngineViewAndDrawStatically {
navController.nav(
R.id.browserFragment,
BrowserFragmentDirections.actionGlobalDownloadsFragment()
)
}
}
}
@ -414,6 +421,7 @@ class DefaultBrowserToolbarController(
ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER
ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS
ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY
ToolbarMenu.Item.Downloads -> Event.BrowserMenuItemTapped.Item.DOWNLOADS
}
activity.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem))

View File

@ -24,7 +24,6 @@ import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.storage.BookmarksStorage
import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
@ -177,11 +176,14 @@ class DefaultToolbarMenu(
?.browsingModeManager?.mode == BrowsingMode.Normal
val shouldDeleteDataOnQuit = context.components.settings
.shouldDeleteBrowsingDataOnQuit
val syncedTabsInTabsTray = context.components.settings
.syncedTabsInTabsTray
val menuItems = listOfNotNull(
if (FeatureFlags.viewDownloads) downloadsItem else null,
historyItem,
bookmarksItem,
if (FeatureFlags.syncedTabs) syncedTabs else null,
if (syncedTabsInTabsTray) null else syncedTabs,
settings,
if (shouldDeleteDataOnQuit) deleteDataOnQuit else null,
BrowserMenuDivider(),
@ -333,6 +335,14 @@ class DefaultToolbarMenu(
onItemTapped.invoke(ToolbarMenu.Item.Bookmarks)
}
val downloadsItem = BrowserMenuImageText(
"Downloads",
R.drawable.ic_download,
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Downloads)
}
@ColorRes
private fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context)

View File

@ -30,6 +30,7 @@ interface ToolbarMenu {
object ReaderModeAppearance : Item()
object Bookmarks : Item()
object History : Item()
object Downloads : Item()
}
val menuBuilder: BrowserMenuBuilder

View File

@ -771,6 +771,15 @@ class HomeFragment : Fragment() {
HomeFragmentDirections.actionGlobalHistoryFragment()
)
}
HomeMenu.Item.Downloads -> {
hideOnboardingIfNeeded()
nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalDownloadsFragment()
)
}
HomeMenu.Item.Help -> {
hideOnboardingIfNeeded()
(activity as HomeActivity).openToBrowserAndLoad(

View File

@ -21,7 +21,6 @@ import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
@ -43,6 +42,7 @@ class HomeMenu(
object SyncedTabs : Item()
object History : Item()
object Bookmarks : Item()
object Downloads : Item()
object Quit : Item()
object Sync : Item()
}
@ -144,6 +144,14 @@ class HomeMenu(
onItemTapped.invoke(Item.Help)
}
val downloadsItem = BrowserMenuImageText(
"Downloads",
R.drawable.ic_download,
primaryTextColor
) {
onItemTapped.invoke(Item.Downloads)
}
// Only query account manager if it has been initialized.
// We don't want to cause its initialization just for this check.
val accountAuthItem = if (context.components.backgroundServices.accountManagerAvailableQueue.isReady()) {
@ -158,9 +166,10 @@ class HomeMenu(
if (settings.shouldDeleteBrowsingDataOnQuit) quitItem else null,
settingsItem,
BrowserMenuDivider(),
if (FeatureFlags.syncedTabs) syncedTabsItem else null,
if (settings.syncedTabsInTabsTray) null else syncedTabsItem,
bookmarksItem,
historyItem,
if (FeatureFlags.viewDownloads) downloadsItem else null,
BrowserMenuDivider(),
addons,
BrowserMenuDivider(),

View File

@ -0,0 +1,41 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.downloads
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.library.SelectionHolder
import org.mozilla.fenix.library.downloads.viewholders.DownloadsListItemViewHolder
class DownloadAdapter(
private val downloadInteractor: DownloadInteractor
) : RecyclerView.Adapter<DownloadsListItemViewHolder>(), SelectionHolder<DownloadItem> {
private var downloads: List<DownloadItem> = listOf()
private var mode: DownloadFragmentState.Mode = DownloadFragmentState.Mode.Normal
override val selectedItems get() = mode.selectedItems
override fun getItemCount(): Int = downloads.size
override fun getItemViewType(position: Int): Int = DownloadsListItemViewHolder.LAYOUT_ID
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadsListItemViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return DownloadsListItemViewHolder(view, downloadInteractor, this)
}
fun updateMode(mode: DownloadFragmentState.Mode) {
this.mode = mode
}
override fun onBindViewHolder(holder: DownloadsListItemViewHolder, position: Int) {
holder.bind(downloads[position])
}
fun updateDownloads(downloads: List<DownloadItem>) {
this.downloads = downloads
notifyDataSetChanged()
}
}

View File

@ -0,0 +1,31 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.downloads
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
interface DownloadController {
fun handleOpen(item: DownloadItem, mode: BrowsingMode? = null)
fun handleBackPressed(): Boolean
}
class DefaultDownloadController(
private val store: DownloadFragmentStore,
private val openToFileManager: (item: DownloadItem, mode: BrowsingMode?) -> Unit
) : DownloadController {
override fun handleOpen(item: DownloadItem, mode: BrowsingMode?) {
openToFileManager(item, mode)
}
override fun handleBackPressed(): Boolean {
return if (store.state.mode is DownloadFragmentState.Mode.Editing) {
store.dispatch(DownloadFragmentAction.ExitEditMode)
true
} else {
false
}
}
}

View File

@ -0,0 +1,108 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.downloads
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_downloads.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.feature.downloads.AbstractFetchDownloadService
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.library.LibraryPageFragment
@SuppressWarnings("TooManyFunctions", "LargeClass")
class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHandler {
private lateinit var downloadStore: DownloadFragmentStore
private lateinit var downloadView: DownloadView
private lateinit var downloadInteractor: DownloadInteractor
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_downloads, container, false)
val items = requireComponents.core.store.state.downloads.map {
DownloadItem(
it.value.id,
it.value.fileName,
it.value.filePath,
it.value.contentLength.toString(),
it.value.contentType
)
}
downloadStore = StoreProvider.get(this) {
DownloadFragmentStore(
DownloadFragmentState(
items = items,
mode = DownloadFragmentState.Mode.Normal
)
)
}
val downloadController: DownloadController = DefaultDownloadController(
downloadStore,
::openItem
)
downloadInteractor = DownloadInteractor(
downloadController
)
downloadView = DownloadView(view.downloadsLayout, downloadInteractor)
return view
}
override val selectedItems get() = downloadStore.state.mode.selectedItems
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requireComponents.analytics.metrics.track(Event.HistoryOpened)
setHasOptionsMenu(false)
}
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
consumeFrom(downloadStore) {
downloadView.update(it)
}
}
override fun onResume() {
super.onResume()
showToolbar(getString(R.string.library_downloads))
}
override fun onBackPressed(): Boolean {
return downloadView.onBackPressed()
}
private fun openItem(item: DownloadItem, mode: BrowsingMode? = null) {
mode?.let { (activity as HomeActivity).browsingModeManager.mode = it }
context?.let {
AbstractFetchDownloadService.openFile(
context = it,
contentType = item.contentType,
filePath = item.filePath
)
}
}
}

View File

@ -0,0 +1,61 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.downloads
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
/**
* Class representing a history entry
* @property id Unique id of the download item
* @property fileName File name of the download item
* @property filePath Full path of the download item
* @property size The size in bytes of the download item
* @property contentType The type of file the download is
*/
data class DownloadItem(val id: Long, val fileName: String?, val filePath: String, val size: String, val contentType: String?)
/**
* The [Store] for holding the [DownloadFragmentState] and applying [DownloadFragmentAction]s.
*/
class DownloadFragmentStore(initialState: DownloadFragmentState) :
Store<DownloadFragmentState, DownloadFragmentAction>(initialState, ::downloadStateReducer)
/**
* Actions to dispatch through the `DownloadStore` to modify `DownloadState` through the reducer.
*/
sealed class DownloadFragmentAction : Action {
object ExitEditMode : DownloadFragmentAction()
}
/**
* The state for the Download Screen
* @property items List of DownloadItem to display
* @property mode Current Mode of Download
*/
data class DownloadFragmentState(
val items: List<DownloadItem>,
val mode: Mode
) : State {
sealed class Mode {
open val selectedItems = emptySet<DownloadItem>()
object Normal : Mode()
data class Editing(override val selectedItems: Set<DownloadItem>) : DownloadFragmentState.Mode()
}
}
/**
* The DownloadState Reducer.
*/
private fun downloadStateReducer(
state: DownloadFragmentState,
action: DownloadFragmentAction
): DownloadFragmentState {
return when (action) {
is DownloadFragmentAction.ExitEditMode -> state.copy(mode = DownloadFragmentState.Mode.Normal)
}
}

View File

@ -0,0 +1,29 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.downloads
/**
* Interactor for the download screen
* Provides implementations for the DownloadViewInteractor
*/
@SuppressWarnings("TooManyFunctions")
class DownloadInteractor(
private val downloadController: DownloadController
) : DownloadViewInteractor {
override fun open(item: DownloadItem) {
downloadController.handleOpen(item)
}
override fun select(item: DownloadItem) {
TODO("Not yet implemented")
}
override fun deselect(item: DownloadItem) {
TODO("Not yet implemented")
}
override fun onBackPressed(): Boolean {
return downloadController.handleBackPressed()
}
}

View File

@ -0,0 +1,74 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.downloads
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import kotlinx.android.synthetic.main.component_downloads.view.*
import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibraryPageView
import org.mozilla.fenix.library.SelectionInteractor
/**
* Interface for the DownloadViewInteractor. This interface is implemented by objects that want
* to respond to user interaction on the DownloadView
*/
interface DownloadViewInteractor : SelectionInteractor<DownloadItem> {
/**
* Called on backpressed to exit edit mode
*/
fun onBackPressed(): Boolean
}
/**
* View that contains and configures the Downloads List
*/
class DownloadView(
container: ViewGroup,
val interactor: DownloadInteractor
) : LibraryPageView(container), UserInteractionHandler {
val view: View = LayoutInflater.from(container.context)
.inflate(R.layout.component_downloads, container, true)
var mode: DownloadFragmentState.Mode = DownloadFragmentState.Mode.Normal
private set
val downloadAdapter = DownloadAdapter(interactor)
private val layoutManager = LinearLayoutManager(container.context)
init {
view.download_list.apply {
layoutManager = this@DownloadView.layoutManager
adapter = downloadAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
}
fun update(state: DownloadFragmentState) {
view.swipe_refresh.isEnabled =
state.mode === DownloadFragmentState.Mode.Normal
mode = state.mode
downloadAdapter.updateMode(state.mode)
downloadAdapter.updateDownloads(state.items)
setUiForNormalMode(
context.getString(R.string.library_downloads)
)
}
override fun onBackPressed(): Boolean {
return interactor.onBackPressed()
}
}

View File

@ -0,0 +1,46 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.downloads.viewholders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.download_list_item.view.*
import kotlinx.android.synthetic.main.library_site_item.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.library.SelectionHolder
import org.mozilla.fenix.library.downloads.DownloadInteractor
import org.mozilla.fenix.library.downloads.DownloadItem
import mozilla.components.feature.downloads.toMegabyteString
class DownloadsListItemViewHolder(
view: View,
private val downloadInteractor: DownloadInteractor,
private val selectionHolder: SelectionHolder<DownloadItem>
) : RecyclerView.ViewHolder(view) {
private var item: DownloadItem? = null
fun bind(
item: DownloadItem
) {
itemView.download_layout.visibility = View.VISIBLE
itemView.download_layout.titleView.text = item.fileName
itemView.download_layout.urlView.text = item.size.toLong().toMegabyteString()
itemView.download_layout.setSelectionInteractor(item, selectionHolder, downloadInteractor)
itemView.download_layout.changeSelected(item in selectionHolder.selectedItems)
itemView.overflow_menu.hideAndDisable()
itemView.favicon.setImageResource(R.drawable.ic_download_default)
itemView.favicon.isClickable = false
this.item = item
}
companion object {
const val LAYOUT_ID = R.layout.download_list_item
}
}

View File

@ -36,5 +36,11 @@ class SecretSettingsFragment : PreferenceFragmentCompat() {
isChecked = context.settings().waitToShowPageUntilFirstPaint
onPreferenceChangeListener = SharedPreferenceUpdater()
}
requirePreference<SwitchPreference>(R.string.pref_key_synced_tabs_tabs_tray).apply {
isVisible = FeatureFlags.syncedTabsInTabsTray
isChecked = context.settings().syncedTabsInTabsTray
onPreferenceChangeListener = SharedPreferenceUpdater()
}
}
}

View File

@ -270,6 +270,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
resources.getString(R.string.pref_key_delete_browsing_data_on_quit_preference) -> {
SettingsFragmentDirections.actionSettingsFragmentToDeleteBrowsingDataOnQuitFragment()
}
resources.getString(R.string.pref_key_notifications) -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
startActivity(intent)
}
null
}
resources.getString(R.string.pref_key_customize) -> {
SettingsFragmentDirections.actionSettingsFragmentToCustomizationFragment()
}
@ -352,6 +359,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
findPreference<Preference>(
getPreferenceKey(R.string.pref_key_debug_settings)
)?.isVisible = requireContext().settings().showSecretDebugMenuThisSession
findPreference<Preference>(
getPreferenceKey(R.string.pref_key_notifications)
)?.isVisible = requireContext().settings().showNotificationsSetting
}
private fun getClickListenerForMakeDefaultBrowser(): Preference.OnPreferenceClickListener {

View File

@ -36,7 +36,6 @@ import mozilla.components.service.fxa.sync.SyncStatusObserver
import mozilla.components.service.fxa.sync.getLastSynced
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.StoreProvider
@ -271,9 +270,8 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
isChecked = syncEnginesStatus.getOrElse(SyncEngine.Passwords) { true }
}
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_tabs).apply {
isVisible = FeatureFlags.syncedTabs
isEnabled = syncEnginesStatus.containsKey(SyncEngine.Tabs)
isChecked = syncEnginesStatus.getOrElse(SyncEngine.Tabs) { FeatureFlags.syncedTabs }
isChecked = syncEnginesStatus.getOrElse(SyncEngine.Tabs) { true }
}
}

View File

@ -10,14 +10,18 @@ import androidx.navigation.NavController
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.ErrorViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.NoTabsViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.TitleViewHolder
import org.mozilla.fenix.sync.ext.toAdapterList
import mozilla.components.browser.storage.sync.Tab as SyncTab
import mozilla.components.concept.sync.Device as SyncDevice
class SyncedTabsAdapter(
private val listener: (SyncTab) -> Unit
private val newListener: SyncedTabsView.Listener
) : ListAdapter<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(DiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder {
@ -27,30 +31,26 @@ class SyncedTabsAdapter(
DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView)
TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView)
ErrorViewHolder.LAYOUT_ID -> ErrorViewHolder(itemView)
TitleViewHolder.LAYOUT_ID -> TitleViewHolder(itemView)
NoTabsViewHolder.LAYOUT_ID -> NoTabsViewHolder(itemView)
else -> throw IllegalStateException()
}
}
override fun onBindViewHolder(holder: SyncedTabsViewHolder, position: Int) {
holder.bind(getItem(position), listener)
holder.bind(getItem(position), newListener)
}
override fun getItemViewType(position: Int) = when (getItem(position)) {
is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID
is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID
is AdapterItem.Error -> ErrorViewHolder.LAYOUT_ID
is AdapterItem.Title -> TitleViewHolder.LAYOUT_ID
is AdapterItem.NoTabs -> NoTabsViewHolder.LAYOUT_ID
}
fun updateData(syncedTabs: List<SyncedDeviceTabs>) {
val allDeviceTabs = mutableListOf<AdapterItem>()
syncedTabs.forEach { (device, tabs) ->
if (tabs.isNotEmpty()) {
allDeviceTabs.add(AdapterItem.Device(device))
tabs.mapTo(allDeviceTabs) { AdapterItem.Tab(it) }
}
}
val allDeviceTabs = syncedTabs.toAdapterList()
submitList(allDeviceTabs)
}
@ -59,7 +59,11 @@ class SyncedTabsAdapter(
when (oldItem) {
is AdapterItem.Device ->
newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id
is AdapterItem.Tab, is AdapterItem.Error ->
is AdapterItem.NoTabs ->
newItem is AdapterItem.NoTabs && oldItem.device.id == newItem.device.id
is AdapterItem.Tab,
is AdapterItem.Error,
is AdapterItem.Title ->
oldItem == newItem
}
@ -68,9 +72,35 @@ class SyncedTabsAdapter(
oldItem == newItem
}
/**
* The various types of adapter items that can be found in a [SyncedTabsAdapter].
*/
sealed class AdapterItem {
/**
* A title header of the Synced Tabs UI that has a refresh button in it. This may be seen
* only in some views depending on where the Synced Tabs UI is displayed.
*/
object Title : AdapterItem()
/**
* A device header for displaying a synced device.
*/
data class Device(val device: SyncDevice) : AdapterItem()
/**
* A tab that was synced.
*/
data class Tab(val tab: SyncTab) : AdapterItem()
/**
* A placeholder for a device that has no tabs synced.
*/
data class NoTabs(val device: SyncDevice) : AdapterItem()
/**
* A message displayed if an error was encountered.
*/
data class Error(
val descriptionResId: Int,
val navController: NavController? = null

View File

@ -13,6 +13,7 @@ import mozilla.components.browser.storage.sync.Tab
import mozilla.components.feature.syncedtabs.SyncedTabsFeature
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
@ -22,6 +23,11 @@ import org.mozilla.fenix.library.LibraryPageFragment
class SyncedTabsFragment : LibraryPageFragment<Tab>() {
private val syncedTabsFeature = ViewBoundFeatureWrapper<SyncedTabsFeature>()
init {
// Sanity-check: Remove this class when the feature flag is always enabled.
FeatureFlags.syncedTabsInTabsTray
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,

View File

@ -7,7 +7,6 @@ package org.mozilla.fenix.sync
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.annotation.StringRes
import androidx.fragment.app.findFragment
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
@ -18,8 +17,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.browser.storage.sync.Tab
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.sync.ext.toAdapterItem
import org.mozilla.fenix.sync.ext.toStringRes
import java.lang.IllegalStateException
class SyncedTabsLayout @JvmOverloads constructor(
@ -30,7 +33,7 @@ class SyncedTabsLayout @JvmOverloads constructor(
override var listener: SyncedTabsView.Listener? = null
private val adapter = SyncedTabsAdapter { listener?.onTabClicked(it) }
private val adapter = SyncedTabsAdapter(ListenerDelegate { listener })
private val coroutineScope = CoroutineScope(Dispatchers.Main)
init {
@ -40,6 +43,9 @@ class SyncedTabsLayout @JvmOverloads constructor(
synced_tabs_list.adapter = adapter
synced_tabs_pull_to_refresh.setOnRefreshListener { listener?.onRefresh() }
// Sanity-check: Remove this class when the feature flag is always enabled.
FeatureFlags.syncedTabsInTabsTray
}
override fun onError(error: SyncedTabsView.ErrorType) {
@ -53,8 +59,8 @@ class SyncedTabsLayout @JvmOverloads constructor(
null
}
val descriptionResId = stringResourceForError(error)
val errorItem = getErrorItem(navController, error, descriptionResId)
val descriptionResId = error.toStringRes()
val errorItem = error.toAdapterItem(descriptionResId, navController)
val errorList: List<SyncedTabsAdapter.AdapterItem> = listOf(errorItem)
adapter.submitList(errorList)
@ -96,27 +102,21 @@ class SyncedTabsLayout @JvmOverloads constructor(
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> true
}
internal fun stringResourceForError(error: SyncedTabsView.ErrorType) = when (error) {
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device
SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing
SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_sign_in_message
SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs
}
internal fun getErrorItem(
navController: NavController?,
error: SyncedTabsView.ErrorType,
@StringRes stringResId: Int
): SyncedTabsAdapter.AdapterItem = when (error) {
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE,
SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION,
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> SyncedTabsAdapter.AdapterItem
.Error(descriptionResId = stringResId)
SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> SyncedTabsAdapter.AdapterItem
.Error(descriptionResId = stringResId, navController = navController)
}
}
}
/**
* We have to do this weird daisy-chaining of callbacks because the listener is nullable and
* when we get a null reference, we never get a new binding to the non-null listener.
*/
class ListenerDelegate(
private val listener: (() -> SyncedTabsView.Listener?)
) : SyncedTabsView.Listener {
override fun onRefresh() {
listener.invoke()?.onRefresh()
}
override fun onTabClicked(tab: Tab) {
listener.invoke()?.onTabClicked(tab)
}
}

View File

@ -7,29 +7,36 @@ package org.mozilla.fenix.sync
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.sync_tabs_error_row.view.*
import kotlinx.android.synthetic.main.sync_tabs_list_item.view.*
import kotlinx.android.synthetic.main.view_synced_tabs_group.view.*
import mozilla.components.browser.storage.sync.Tab
import kotlinx.android.synthetic.main.view_synced_tabs_title.view.*
import mozilla.components.concept.sync.DeviceType
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem
/**
* The various view-holders that can be found in a [SyncedTabsAdapter]. For more
* descriptive information on the different types, see the docs for [AdapterItem].
*/
sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit)
abstract fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener)
class TabViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit) {
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
bindTab(item as AdapterItem.Tab)
itemView.setOnClickListener {
interactor(item.tab)
interactor.onTabClicked(item.tab)
}
}
@ -46,7 +53,7 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
class ErrorViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit) {
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
val errorItem = item as AdapterItem.Error
setErrorMargins()
@ -69,7 +76,7 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
class DeviceViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit) {
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
bindHeader(item as AdapterItem.Device)
}
@ -93,6 +100,36 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
}
}
class NoTabsViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) = Unit
companion object {
const val LAYOUT_ID = R.layout.view_synced_tabs_no_item
}
}
class TitleViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
itemView.refresh_icon.setOnClickListener { v ->
val rotation = AnimationUtils.loadAnimation(
itemView.context,
R.anim.full_rotation
).apply {
repeatCount = Animation.ABSOLUTE
}
v.startAnimation(rotation)
interactor.onRefresh()
}
}
companion object {
const val LAYOUT_ID = R.layout.view_synced_tabs_title
}
}
internal fun setErrorMargins() {
val lp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,

View File

@ -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.sync.ext
import androidx.annotation.StringRes
import androidx.navigation.NavController
import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
import org.mozilla.fenix.R
import org.mozilla.fenix.sync.SyncedTabsAdapter
/**
* Converts the error type to the appropriate matching string resource for displaying to the user.
*/
fun ErrorType.toStringRes() = when (this) {
ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device
ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing
ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_sign_in_message
ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth
ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs
}
/**
* Converts an error type to an [SyncedTabsAdapter.AdapterItem.Error].
*/
fun ErrorType.toAdapterItem(
@StringRes stringResId: Int,
navController: NavController? = null
) = when (this) {
ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
ErrorType.SYNC_ENGINE_UNAVAILABLE,
ErrorType.SYNC_NEEDS_REAUTHENTICATION,
ErrorType.NO_TABS_AVAILABLE -> SyncedTabsAdapter.AdapterItem
.Error(descriptionResId = stringResId)
ErrorType.SYNC_UNAVAILABLE -> SyncedTabsAdapter.AdapterItem
.Error(descriptionResId = stringResId, navController = navController)
}

View File

@ -0,0 +1,22 @@
/* 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.sync.ext
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem
/**
* Converts a list of [SyncedDeviceTabs] into a list of [AdapterItem].
*/
fun List<SyncedDeviceTabs>.toAdapterList() = asSequence().flatMap { (device, tabs) ->
val deviceTabs = if (tabs.isEmpty()) {
sequenceOf(AdapterItem.NoTabs(device))
} else {
tabs.asSequence().map { AdapterItem.Tab(it) }
}
sequenceOf(AdapterItem.Device(device)) + deviceTabs
}.toList()

View File

@ -0,0 +1,85 @@
/* 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.tabtray
import android.view.View
import androidx.fragment.app.FragmentManager.findFragment
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ConcatAdapter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.sync.ListenerDelegate
import org.mozilla.fenix.sync.SyncedTabsAdapter
import org.mozilla.fenix.sync.ext.toAdapterList
import org.mozilla.fenix.sync.ext.toAdapterItem
import org.mozilla.fenix.sync.ext.toStringRes
import kotlin.coroutines.CoroutineContext
@OptIn(ExperimentalCoroutinesApi::class)
class SyncedTabsController(
lifecycleOwner: LifecycleOwner,
private val view: View,
store: TabTrayDialogFragmentStore,
private val concatAdapter: ConcatAdapter,
coroutineContext: CoroutineContext = Dispatchers.Main
) : SyncedTabsView {
override var listener: SyncedTabsView.Listener? = null
val adapter = SyncedTabsAdapter(ListenerDelegate { listener })
private val scope: CoroutineScope = CoroutineScope(coroutineContext)
init {
store.flowScoped(lifecycleOwner) { flow ->
flow.map { it.mode }
.ifChanged()
.drop(1)
.collect { mode ->
when (mode) {
is TabTrayDialogFragmentState.Mode.Normal -> {
concatAdapter.addAdapter(0, adapter)
}
is TabTrayDialogFragmentState.Mode.MultiSelect -> {
concatAdapter.removeAdapter(adapter)
}
}
}
}
}
override fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>) {
scope.launch {
val tabsList = listOf(SyncedTabsAdapter.AdapterItem.Title) + syncedTabs.toAdapterList()
// Reverse layout for TabTrayView which does things backwards.
adapter.submitList(tabsList.reversed())
}
}
override fun onError(error: SyncedTabsView.ErrorType) {
scope.launch {
val navController: NavController? = try {
findFragment<TabTrayDialogFragment>(view).findNavController()
} catch (exception: IllegalStateException) {
null
}
val descriptionResId = error.toStringRes()
val errorItem = error.toAdapterItem(descriptionResId, navController)
adapter.submitList(listOf(errorItem))
}
}
}

View File

@ -9,10 +9,12 @@ import androidx.navigation.NavController
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.storage.sync.Tab as SyncTab
import mozilla.components.concept.engine.profiler.Profiler
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.tabstray.Tab
import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
@ -30,6 +32,7 @@ interface TabTrayController {
fun onNewTabTapped(private: Boolean)
fun onTabTrayDismissed()
fun onShareTabsClicked(private: Boolean)
fun onSyncedTabClicked(syncTab: SyncTab)
fun onSaveToCollectionClicked(selectedTabs: Set<Tab>)
fun onCloseAllTabsClicked(private: Boolean)
fun handleBackPressed(): Boolean
@ -59,6 +62,7 @@ interface TabTrayController {
*/
@Suppress("TooManyFunctions")
class DefaultTabTrayController(
private val activity: HomeActivity,
private val profiler: Profiler?,
private val sessionManager: SessionManager,
private val browsingModeManager: BrowsingModeManager,
@ -117,6 +121,14 @@ class DefaultTabTrayController(
navController.navigate(directions)
}
override fun onSyncedTabClicked(syncTab: SyncTab) {
activity.openToBrowserAndLoad(
searchTermOrURL = syncTab.active().url,
newTab = true,
from = BrowserDirection.FromTabTray
)
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun onCloseAllTabsClicked(private: Boolean) {
val sessionsToClose = if (private) {

View File

@ -177,6 +177,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
adapter,
interactor = TabTrayFragmentInteractor(
DefaultTabTrayController(
activity = activity,
profiler = activity.components.core.engine.profiler,
sessionManager = activity.components.core.sessionManager,
browsingModeManager = activity.browsingModeManager,
@ -191,10 +192,11 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
showAddNewCollectionDialog = ::showAddNewCollectionDialog
)
),
store = tabTrayDialogStore,
isPrivate = isPrivate,
startingInLandscape = requireContext().resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE,
lifecycleScope = viewLifecycleOwner.lifecycleScope
lifecycleOwner = viewLifecycleOwner
) { private ->
val filter: (TabSessionState) -> Boolean = { state -> private == state.content.private }

View File

@ -5,6 +5,7 @@
package org.mozilla.fenix.tabtray
import mozilla.components.concept.tabstray.Tab
import mozilla.components.browser.storage.sync.Tab as SyncTab
@Suppress("TooManyFunctions")
interface TabTrayInteractor {
@ -33,6 +34,11 @@ interface TabTrayInteractor {
*/
fun onCloseAllTabsClicked(private: Boolean)
/**
* Called when the user clicks on a synced tab entry.
*/
fun onSyncedTabClicked(syncTab: SyncTab)
/**
* Called when the physical back button is clicked.
*/
@ -89,6 +95,10 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab
controller.onCloseAllTabsClicked(private)
}
override fun onSyncedTabClicked(syncTab: SyncTab) {
controller.onSyncedTabClicked(syncTab)
}
override fun onBackPressed(): Boolean {
return controller.handleBackPressed()
}

View File

@ -16,7 +16,8 @@ import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -35,7 +36,10 @@ import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.storage.sync.Tab as SyncTab
import mozilla.components.browser.tabstray.TabViewHolder
import mozilla.components.feature.syncedtabs.SyncedTabsFeature
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
@ -56,11 +60,13 @@ class TabTrayView(
private val container: ViewGroup,
private val tabsAdapter: FenixTabsAdapter,
private val interactor: TabTrayInteractor,
store: TabTrayDialogFragmentStore,
isPrivate: Boolean,
startingInLandscape: Boolean,
lifecycleScope: LifecycleCoroutineScope,
lifecycleOwner: LifecycleOwner,
private val filterTabs: (Boolean) -> Unit
) : LayoutContainer, TabLayout.OnTabSelectedListener {
val lifecycleScope = lifecycleOwner.lifecycleScope
val fabView = LayoutInflater.from(container.context)
.inflate(R.layout.component_tabstray_fab, container, true)
@ -73,19 +79,25 @@ class TabTrayView(
private val behavior = BottomSheetBehavior.from(view.tab_wrapper)
private val concatAdapter = ConcatAdapter(tabsAdapter)
private val tabTrayItemMenu: TabTrayItemMenu
private var menu: BrowserMenu? = null
private var tabsTouchHelper: TabsTouchHelper
private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate)
private val syncedTabsController = SyncedTabsController(lifecycleOwner, view, store, concatAdapter)
private val syncedTabsFeature = ViewBoundFeatureWrapper<SyncedTabsFeature>()
private var hasLoaded = false
override val containerView: View?
get() = container
private val components = container.context.components
init {
container.context.components.analytics.metrics.track(Event.TabsTrayOpened)
components.analytics.metrics.track(Event.TabsTrayOpened)
toggleFabText(isPrivate)
@ -102,7 +114,7 @@ class TabTrayView(
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
container.context.components.analytics.metrics.track(Event.TabsTrayClosed)
components.analytics.metrics.track(Event.TabsTrayClosed)
interactor.onTabTrayDismissed()
}
}
@ -135,7 +147,20 @@ class TabTrayView(
setTopOffset(startingInLandscape)
val concatAdapter = ConcatAdapter(tabsAdapter)
if (view.context.settings().syncedTabsInTabsTray) {
syncedTabsFeature.set(
feature = SyncedTabsFeature(
context = container.context,
storage = components.backgroundServices.syncedTabsStorage,
accountManager = components.backgroundServices.accountManager,
view = syncedTabsController,
lifecycleOwner = lifecycleOwner,
onTabClicked = ::handleTabClicked
),
owner = lifecycleOwner,
view = view
)
}
view.tabsTray.apply {
layoutManager = LinearLayoutManager(container.context).apply {
@ -156,6 +181,9 @@ class TabTrayView(
// Put the 'Add to collections' button after the tabs have loaded.
concatAdapter.addAdapter(0, collectionsButtonAdapter)
// Put the Synced Tabs adapter at the end.
concatAdapter.addAdapter(0, syncedTabsController.adapter)
if (hasAccessibilityEnabled) {
tabsAdapter.notifyDataSetChanged()
}
@ -193,7 +221,7 @@ class TabTrayView(
}
view.tab_tray_overflow.setOnClickListener {
container.context.components.analytics.metrics.track(Event.TabsTrayMenuOpened)
components.analytics.metrics.track(Event.TabsTrayMenuOpened)
menu = tabTrayItemMenu.menuBuilder.build(container.context)
menu?.show(it)
?.also { pu ->
@ -209,6 +237,10 @@ class TabTrayView(
adjustNewTabButtonsForNormalMode()
}
private fun handleTabClicked(tab: SyncTab) {
interactor.onSyncedTabClicked(tab)
}
private fun adjustNewTabButtonsForNormalMode() {
view.tab_tray_new_tab.apply {
isVisible = hasAccessibilityEnabled
@ -234,7 +266,7 @@ class TabTrayView(
Event.NewTabTapped
}
container.context.components.analytics.metrics.track(eventToSend)
components.analytics.metrics.track(eventToSend)
}
fun expand() {
@ -261,17 +293,14 @@ class TabTrayView(
scrollToTab(view.context.components.core.store.state.selectedTabId)
if (isPrivateModeSelected) {
container.context.components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped)
components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped)
} else {
container.context.components.analytics.metrics.track(Event.TabsTrayNormalModeTapped)
components.analytics.metrics.track(Event.TabsTrayNormalModeTapped)
}
}
override fun onTabReselected(tab: TabLayout.Tab?) { /*noop*/
}
override fun onTabUnselected(tab: TabLayout.Tab?) { /*noop*/
}
override fun onTabReselected(tab: TabLayout.Tab?) = Unit
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
var mode: Mode = Mode.Normal
private set
@ -513,7 +542,9 @@ class TabTrayView(
// We offset the tab index by the number of items in the other adapters.
// We add the offset, because the layoutManager is initialized with `reverseLayout`.
val recyclerViewIndex = selectedBrowserTabIndex + collectionsButtonAdapter.itemCount
val recyclerViewIndex = selectedBrowserTabIndex +
collectionsButtonAdapter.itemCount +
syncedTabsController.adapter.itemCount
layoutManager?.scrollToPosition(recyclerViewIndex)
}

View File

@ -109,6 +109,12 @@ class Settings(private val appContext: Context) : PreferencesHolder {
featureFlag = FeatureFlags.waitUntilPaintToDraw
)
var syncedTabsInTabsTray by featureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_synced_tabs_tabs_tray),
default = false,
featureFlag = FeatureFlags.syncedTabsInTabsTray
)
var forceEnableZoom by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_accessibility_force_enable_zoom),
default = false
@ -279,6 +285,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
!trackingProtectionOnboardingShownThisSession)
var showSecretDebugMenuThisSession = false
var showNotificationsSetting = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
val shouldShowSecurityPinWarningSync: Boolean
get() = loginsSecureWarningSyncCount < showLoginsSecureWarningSyncMaxCount

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="275"
android:fromDegrees="0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="360" />

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/download_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progress_bar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="8dp"
android:translationY="-3dp"
android:visibility="gone"
android:indeterminate="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/download_empty_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
android:text="@string/download_empty_message"
android:textColor="?secondaryText"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/download_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
tools:listitem="@layout/download_list_item"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="wrap_content"
android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
<org.mozilla.fenix.library.LibrarySiteItemView
android:id="@+id/download_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/library_item_height" />
</LinearLayout>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/downloadsLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="org.mozilla.fenix.library.downloads.DownloadFragment" />

View File

@ -19,6 +19,7 @@
android:layout_marginTop="4dp"
android:textSize="14sp"
android:textAlignment="viewStart"
android:textColor="@color/tab_tray_item_text_normal_theme"
tools:text="@string/synced_tabs_no_tabs"/>
<com.google.android.material.button.MaterialButton

View File

@ -6,6 +6,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:paddingTop="6dp"
android:paddingBottom="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground">
@ -16,7 +18,6 @@
android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginEnd="48dp"
android:layout_marginTop="6dp"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?primaryText"
@ -42,16 +43,4 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/synced_tab_item_title" />
<View
android:id="@+id/synced_tab_item_separator"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="7dp"
android:background="?syncedTabsSeparator"
android:importantForAccessibility="no"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/synced_tab_item_url" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginTop="16dp"
android:singleLine="true"
android:text="@string/synced_tabs_no_open_tabs"
android:textAlignment="viewStart"
android:textColor="?secondaryText"
android:textSize="12sp" />
</FrameLayout>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
style="@style/Header16TextStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="60dp"
android:layout_weight="1"
android:text="@string/synced_tabs" />
<ImageView
android:id="@+id/refresh_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="60dp"
android:layout_marginEnd="16dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
app:srcCompat="@drawable/mozac_ic_refresh"
app:tint="?primaryText" />
</LinearLayout>

View File

@ -70,6 +70,9 @@
<action
android:id="@+id/action_global_historyFragment"
app:destination="@id/historyFragment" />
<action android:id="@+id/action_global_downloadsFragment"
app:destination="@id/downloadsFragment" />
<action
android:id="@+id/action_global_accountProblemFragment"
app:destination="@id/accountProblemFragment" />
@ -239,6 +242,12 @@
android:label="@string/library_history"
tools:layout="@layout/fragment_history" />
<fragment
android:id="@+id/downloadsFragment"
android:name="org.mozilla.fenix.library.downloads.DownloadFragment"
android:label="Downloads"
tools:layout="@layout/fragment_downloads" />
<fragment
android:id="@+id/bookmarkFragment"
android:name="org.mozilla.fenix.library.bookmarks.BookmarkFragment"

View File

@ -37,6 +37,8 @@
<!-- Content description for checkmark while tab is selected while in multiselect mode in tab tray. The first parameter is the title of the tab selected -->
<string name="tab_tray_item_selected_multiselect_content_description">Označeno %1$s</string>
<!-- Content description when tab is unselected while in multiselect mode in tab tray. The first parameter is the title of the tab unselected -->
<string name="tab_tray_item_unselected_multiselect_content_description">Neizabrano %1$s</string>
<!-- Content description announcement when exiting multiselect mode in tab tray -->
<string name="tab_tray_exit_multiselect_content_description">Izašao iz režima s više izbora</string>
<!-- Content description announcement when entering multiselect mode in tab tray -->
@ -164,8 +166,8 @@
<!-- Search Fragment -->
<!-- Button in the search view that lets a user search by scanning a QR code -->
<string name="search_scan_button">Skeniraj</string>
<!-- Button in the search view that lets a user search by using a shortcut -->
<string name="search_engines_shortcut_button">Pretraživač</string>
<!-- Button in the search view that lets a user change their search engine -->
<string name="search_engine_button">Pretraživač</string>
<!-- Button in the search view when shortcuts are displayed that takes a user to the search engine settings -->
<string name="search_shortcuts_engine_settings">Postavke pretraživača</string>
<!-- Header displayed when selecting a shortcut search engine -->
@ -294,6 +296,8 @@
<string name="preferences_account_settings">Postavke računa</string>
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Otvori linkove u aplikacijama</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Vanjski menadžer preuzimanja</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Add-oni</string>
@ -1442,9 +1446,7 @@
<string name="saved_login_duplicate">Prijava sa tim korisničkim imenom već postoji</string>
<!-- Synced Tabs -->
<!-- Text displayed when user is not logged into a Firefox Account -->
<string name="synced_tabs_connect_to_sync_account">Povežite se sa Firefox računom.</string>
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<string name="synced_tabs_connect_another_device">Povežite drugi uređaj</string>
<!-- Text displayed asking user to re-authenticate -->
<string name="synced_tabs_reauth">Ponovo potvrdite identitet.</string>
@ -1465,13 +1467,4 @@
<!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">OK, razumijem</string>
<!-- DEPRECATED STRINGS -->
<!-- Button in the search view that lets a user search by using a shortcut -->
<string name="search_shortcuts_button">Prečice</string>
<!-- DEPRECATED: Header displayed when selecting a shortcut search engine -->
<string name="search_shortcuts_search_with">Traži na</string>
<!-- Header displayed when selecting a shortcut search engine -->
<string name="search_shortcuts_search_with_2">Ovaj put, traži na:</string>
<!-- Preference title for switch preference to show search shortcuts -->
<string name="preferences_show_search_shortcuts">Prikaži prečice za pretraživanje</string>
</resources>

View File

@ -34,6 +34,11 @@
<!-- Label of button in save to collection dialog for selecting a current collection -->
<string name="tab_tray_select_collection">Επιλογή συλλογής</string>
<!-- Content description for close button while in multiselect mode in tab tray -->
<string name="tab_tray_close_multiselect_content_description">Τέλος λειτουργίας πολλαπλής επιλογής</string>
<!-- Content description for save to collection button while in multiselect mode in tab tray -->
<string name="tab_tray_collection_button_multiselect_content_description">Αποθήκευση επιλεγμένων καρτελών στη συλλογή</string>
<!-- About content. The first parameter is the name of the application. (For example: Fenix) -->
<string name="about_content">Το %1$s αναπτύσσεται από τη Mozilla.</string>
@ -255,6 +260,8 @@
<string name="developer_tools_category">Εργαλεία προγραμματιστή</string>
<!-- Preference for developers -->
<string name="preferences_remote_debugging">Απομακρυσμένος εντοπισμός σφαλμάτων μέσω USB</string>
<!-- Preference title for switch preference to show search engines -->
<string name="preferences_show_search_engines">Εμφάνιση μηχανών αναζήτησης</string>
<!-- Preference title for switch preference to show search suggestions -->
<string name="preferences_show_search_suggestions">Εμφάνιση προτάσεων αναζήτησης</string>
<!-- Preference title for switch preference to show voice search button -->
@ -271,6 +278,8 @@
<string name="preferences_account_settings">Ρυθμίσεις λογαριασμού</string>
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Άνοιγμα συνδέσμων σε εφαρμογές</string>
<!-- Preference for open download with an external download manager app -->
<string name="preferences_external_download_manager">Εξωτερική διαχείριση λήψεων</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Πρόσθετα</string>
@ -322,17 +331,25 @@
<string name="preferences_tracking_protection">Προστασία από καταγραφή</string>
<!-- Preference for tracking protection exceptions -->
<string name="preferences_tracking_protection_exceptions">Εξαιρέσεις</string>
<!-- Button in Exceptions Preference to turn on tracking protection for all sites (remove all exceptions) -->
<string name="preferences_tracking_protection_exceptions_turn_on_for_all">Ενεργοποίηση για όλες τις σελίδες</string>
<!-- Text displayed when there are no exceptions, with learn more link that brings users to a tracking protection SUMO page -->
<string name="exceptions_empty_message_learn_more_link">Μάθετε περισσότερα</string>
<!-- Preference switch for Telemetry -->
<string name="preferences_telemetry">Τηλεμετρία</string>
<!-- Preference switch for usage and technical data collection -->
<string name="preference_usage_data">Δεδομένα χρήσης και τεχνικά δεδομένα</string>
<!-- Preference switch for marketing data collection -->
<string name="preferences_marketing_data">Δεδομένα μάρκετινγκ</string>
<!-- Title for experiments preferences -->
<string name="preference_experiments">Πειράματα</string>
<!-- Summary for experiments preferences -->
<string name="preference_experiments_summary">Επιτρέπει στη Mozilla την εγκατάσταση και συλλογή δεδομένων για πειραματικές λειτουργίες</string>
<!-- Preference switch for crash reporter -->
<string name="preferences_crash_reporter">Αναφορά καταρρεύσεων</string>
<!-- Preference switch for Mozilla location service -->
<string name="preferences_mozilla_location_service">Υπηρεσία τοποθεσίας Mozilla</string>
<!-- Preference switch for app health report. The first parameter is the name of the application (For example: Fenix) -->
@ -645,6 +662,8 @@
<string name="tracking_protection_off">Ανενεργό</string>
<!-- Label that indicates that all video and audio autoplay is allowed -->
<string name="preference_option_autoplay_allowed2">Αποδοχή ήχου και βίντεο</string>
<!-- Subtext that explains 'autoplay on Wi-Fi only' option -->
<string name="preference_option_autoplay_allowed_wifi_subtext">Η αναπαραγωγή ήχων/βίντεο θα γίνεται σε Wi-Fi</string>
<!-- Label that indicates that video autoplay is allowed, but audio autoplay is blocked -->
<string name="preference_option_autoplay_block_audio2">Φραγή ήχου μόνο</string>
<!-- Label that indicates that all video and audio autoplay is blocked -->
@ -661,6 +680,8 @@
<string name="collection_menu_button_content_description">Μενού συλλογής</string>
<!-- No Open Tabs Message Header -->
<string name="no_collections_header1">Συλλέξτε όλα όσα έχουν σημασία για εσάς</string>
<!-- Label to describe what collections are to a new user without any collections -->
<string name="no_collections_description1">Ομαδοποιήστε παρόμοιες αναζητήσεις, σελίδες και καρτέλες για γρήγορη πρόσβαση αργότερα.</string>
<!-- Title for the "select tabs" step of the collection creator -->
<string name="create_collection_select_tabs">Επιλέξτε καρτέλες</string>
<!-- Title for the "select collection" step of the collection creator -->
@ -681,6 +702,12 @@
<!-- Text to show users they have one tab selected in the "select tabs" step of the collection creator.
%d is a placeholder for the number of tabs selected. -->
<string name="create_collection_save_to_collection_tab_selected">Επιλέχθηκε %d καρτέλα</string>
<!-- Text shown in snackbar when multiple tabs have been saved in a collection -->
<string name="create_collection_tabs_saved">Οι καρτέλες αποθηκεύτηκαν!</string>
<!-- Text shown in snackbar when one or multiple tabs have been saved in a new collection -->
<string name="create_collection_tabs_saved_new_collection">Η συλλογή αποθηκεύτηκε!</string>
<!-- Text shown in snackbar when one tab has been saved in a collection -->
<string name="create_collection_tab_saved">Η καρτέλα αποθηκεύτηκε!</string>
<!-- Content description (not visible, for screen readers etc.): button to close the collection creator -->
<string name="create_collection_close">Κλείσιμο</string>
<!-- Button to save currently selected tabs in the "select tabs" step of the collection creator-->
@ -718,8 +745,12 @@
<string name="sync_offline">Εκτός σύνδεσης</string>
<!-- An option to connect additional devices -->
<string name="sync_connect_device">Σύνδεση άλλης συσκευής</string>
<!-- The dialog text shown when additional devices are not available -->
<string name="sync_connect_device_dialog">Για να στείλετε μια καρτέλα, συνδεθείτε στο Firefox σε άλλη μία τουλάχιστον συσκευή.</string>
<!-- Confirmation dialog button -->
<string name="sync_confirmation_button">Το κατάλαβα</string>
<!-- Share error message -->
<string name="share_error_snackbar">Αδυναμία κοινοποίησης σε αυτή την εφαρμογή</string>
<!-- Add new device screen title -->
<string name="sync_add_new_device_title">Αποστολή σε συσκευή</string>
@ -847,17 +878,29 @@
<!-- text for firefox preview moving tip header "Firefox Preview" and "Firefox Nightly" are intentionally hardcoded -->
<string name="tip_firefox_preview_moved_header">Το Firefox Preview είναι πλέον το Firefox Nightly</string>
<!-- text for firefox preview moving tip description -->
<string name="tip_firefox_preview_moved_description">
Το Firefox Nightly ενημερώνεται κάθε βράδυ και διαθέτει νέες, πειραματικές λειτουργίες.
Ωστόσο, ενδέχεται να είναι λιγότερο σταθερό. Κάντε λήψη του beta προγράμματος περιήγησής μας για μια πιο σταθερή εμπειρία.</string>
<!-- text for firefox preview moving tip button. "Firefox for Android Beta" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_button_2">Λήψη του Firefox για Android Beta</string>
<!-- text for firefox preview moving tip header. "Firefox Nightly" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_header_preview_installed">Το Firefox Nightly έχει μετακινηθεί</string>
<!-- text for firefox preview moving tip description -->
<string name="tip_firefox_preview_moved_description_preview_installed">
Αυτή η εφαρμογή δεν θα λαμβάνει πλέον ενημερώσεις ασφαλείας. Σταματήστε τη χρήση αυτής της εφαρμογής και μεταβείτε στο νέο Nightly.
\n\nΓια να μεταφέρετε τους σελιδοδείκτες, τις συνδέσεις και το ιστορικό σας σε άλλη εφαρμογή, δημιουργήστε ένα λογαριασμό Firefox.</string>
<!-- text for firefox preview moving tip button -->
<string name="tip_firefox_preview_moved_button_preview_installed">Εναλλαγή στο νέο Nightly</string>
<!-- text for firefox preview moving tip header. "Firefox Nightly" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_header_preview_not_installed">Το Firefox Nightly έχει μετακινηθεί</string>
<!-- text for firefox preview moving tip description -->
<string name="tip_firefox_preview_moved_description_preview_not_installed">
Αυτή η εφαρμογή δεν θα λαμβάνει πλέον ενημερώσεις ασφαλείας. Αποκτήστε το νέο Nightly και σταματήστε τη χρήση αυτής της εφαρμογής.
\n\nΓια να μεταφέρετε τους σελιδοδείκτες, τις συνδέσεις και το ιστορικό σας σε άλλη εφαρμογή, δημιουργήστε ένα λογαριασμό Firefox.</string>
<!-- text for firefox preview moving tip button -->
<string name="tip_firefox_preview_moved_button_preview_not_installed">Αποκτήστε το νέο Nightly</string>
@ -872,6 +915,11 @@
<string name="onboarding_feature_section_header">Γνωρίστε το %s</string>
<!-- text for the "What's New" onboarding card header -->
<string name="onboarding_whats_new_header1">Δείτε τι νέο υπάρχει</string>
<!-- text for the "what's new" onboarding card description
The first parameter is the short name of the app (e.g. Firefox) -->
<string name="onboarding_whats_new_description">Έχετε ερωτήσεις σχετικά με το επανασχεδιασμένο %s; Θέλετε να μάθετε τι έχει αλλάξει;</string>
<!-- text for underlined clickable link that is part of "what's new" onboarding card description that links to an FAQ -->
<string name="onboarding_whats_new_description_linktext">Λάβετε απαντήσεις εδώ</string>
<!-- text for the firefox account onboarding card header
The first parameter is the name of the app (e.g. Firefox Preview) -->
<string name="onboarding_firefox_account_header">Αξιοποιήστε στο έπακρο το %s.</string>
@ -888,6 +936,9 @@
<!-- text for the tracking protection onboarding card header -->
<string name="onboarding_tracking_protection_header_2">Αυτόματο απόρρητο</string>
<!-- text for the tracking protection card description
The first parameter is the name of the app (e.g. Firefox Preview) -->
<string name="onboarding_tracking_protection_description_2">Οι ρυθμίσεις απορρήτου και ασφάλειας αποκλείουν ιχνηλάτες, κακόβουλο λογισμικό και εταιρείες που σας ακολουθούν.</string>
<!-- text for tracking protection radio button option for standard level of blocking -->
<string name="onboarding_tracking_protection_standard_button_2">Τυπική (προεπιλογή)</string>
<!-- text for tracking protection radio button option for strict level of blocking -->
@ -900,6 +951,8 @@
<string name="onboarding_private_browsing_button">Άνοιγμα ρυθμίσεων</string>
<!-- text for the privacy notice onboarding card header -->
<string name="onboarding_privacy_notice_header">Το απόρρητό σας</string>
<!-- Text for the button to read the privacy notice -->
<string name="onboarding_privacy_notice_read_button">Διαβάστε τη σημείωση απορρήτου μας</string>
<!-- Content description (not visible, for screen readers etc.): Close onboarding screen -->
<string name="onboarding_close">Κλείσιμο</string>
@ -918,6 +971,10 @@
<!-- Theme setting for light mode -->
<string name="onboarding_theme_light_title">Φωτεινό θέμα</string>
<!-- Text shown in snackbar when multiple tabs have been sent to device -->
<string name="sync_sent_tabs_snackbar">Οι καρτέλες απεστάλησαν!</string>
<!-- Text shown in snackbar when one tab has been sent to device -->
<string name="sync_sent_tab_snackbar">Η καρτέλα απεστάλη!</string>
<!-- Text shown in snackbar when sharing tabs failed -->
<string name="sync_sent_tab_error_snackbar">Δεν ήταν δυνατή η αποστολή</string>
<!-- Text shown in snackbar for the "retry" action that the user has after sharing tabs failed -->
@ -933,11 +990,18 @@
<!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Χρήση email</string>
<!-- Text shown in confirmation dialog to sign out of account -->
<string name="sign_out_confirmation_message">Το Firefox θα σταματήσει να συγχρονίζεται με το λογαριασμό σας, αλλά δεν θα διαγράψει τα δεδομένα περιήγησης από αυτή τη συσκευή.</string>
<!-- Text shown in confirmation dialog to sign out of account. The first parameter is the name of the app (e.g. Firefox Preview) -->
<string name="sign_out_confirmation_message_2">Το %s θα σταματήσει να συγχρονίζεται με το λογαριασμό σας, αλλά δεν θα διαγράψει τα δεδομένα περιήγησης από αυτή τη συσκευή.</string>
<!-- Option to continue signing out of account shown in confirmation dialog to sign out of account -->
<string name="sign_out_disconnect">Αποσύνδεση</string>
<!-- Option to cancel signing out shown in confirmation dialog to sign out of account -->
<string name="sign_out_cancel">Ακύρωση</string>
<!-- Error message snackbar shown after the user tried to select a default folder which cannot be altered -->
<string name="bookmark_cannot_edit_root">Δεν είναι δυνατή η επεξεργασία προεπιλεγμένων φακέλων</string>
<!-- Enhanced Tracking Protection -->
<!-- Link displayed in enhanced tracking protection panel to access tracking protection settings -->
<string name="etp_settings">Ρυθμίσεις προστασίας</string>
@ -951,12 +1015,22 @@
<string name="preference_enhanced_tracking_protection_explanation_learn_more">Μάθετε περισσότερα</string>
<!-- Preference for enhanced tracking protection for the standard protection settings -->
<string name="preference_enhanced_tracking_protection_standard_default_1">Τυπική (προεπιλογή)</string>
<!-- Preference description for enhanced tracking protection for the standard protection settings -->
<string name="preference_enhanced_tracking_protection_standard_description_3">Φραγή λιγότερων ιχνηλατών. Οι σελίδες θα φορτώνονται κανονικά.</string>
<!-- Accessibility text for the Standard protection information icon -->
<string name="preference_enhanced_tracking_protection_standard_info_button">Τι αποκλείει η τυπική προστασία από καταγραφή</string>
<!-- Preference for enhanced tracking protection for the strict protection settings -->
<string name="preference_enhanced_tracking_protection_strict">Αυστηρή</string>
<!-- Preference description for enhanced tracking protection for the strict protection settings -->
<string name="preference_enhanced_tracking_protection_strict_description_2">Φραγή περισσότερων ιχνηλατών, διαφημίσεων και αναδυόμενων παραθύρων. Οι σελίδες φορτώνονται ταχύτερα, αλλά ορισμένα μέρη ενδέχεται να μην λειτουργούν.</string>
<!-- Accessibility text for the Strict protection information icon -->
<string name="preference_enhanced_tracking_protection_strict_info_button">Τι αποκλείει η αυστηρή προστασία από καταγραφή</string>
<!-- Preference for enhanced tracking protection for the custom protection settings -->
<string name="preference_enhanced_tracking_protection_custom">Προσαρμοσμένη</string>
<!-- Preference description for enhanced tracking protection for the strict protection settings -->
<string name="preference_enhanced_tracking_protection_custom_description_2">Επιλέξτε ιχνηλάτες και σενάρια για αποκλεισμό.</string>
<!-- Accessibility text for the Strict protection information icon -->
<string name="preference_enhanced_tracking_protection_custom_info_button">Τι αποκλείει η προσαρμοσμένη προστασία από καταγραφή</string>
<!-- Header for categories that are being blocked by current Enhanced Tracking Protection settings -->
<!-- Preference for enhanced tracking protection for the custom protection settings for cookies-->
<string name="preference_enhanced_tracking_protection_custom_cookies">Cookies</string>
@ -980,6 +1054,9 @@
<string name="preference_enhanced_tracking_protection_custom_cryptominers">Cryptominers</string>
<!-- Preference for enhanced tracking protection for the custom protection settings -->
<string name="preference_enhanced_tracking_protection_custom_fingerprinters">Fingerprinters</string>
<string name="enhanced_tracking_protection_blocked">Αποκλείεται</string>
<!-- Header for categories that are being not being blocked by current Enhanced Tracking Protection settings -->
<string name="enhanced_tracking_protection_allowed">Επιτρέπεται</string>
<!-- Category of trackers (social media trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_social_media_trackers_title">Ιχνηλάτες κοινωνικών δικτύων</string>
<!-- Category of trackers (cross-site tracking cookies) that can be blocked by Enhanced Tracking Protection -->
@ -990,6 +1067,15 @@
<string name="etp_fingerprinters_title">Fingerprinters</string>
<!-- Category of trackers (tracking content) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_tracking_content_title">Περιεχόμενο καταγραφής</string>
<!-- Enhanced Tracking Protection message that protection is currently on for this site -->
<string name="etp_panel_on">Η προστασία είναι ΕΝΕΡΓΗ για αυτή τη σελίδα</string>
<!-- Enhanced Tracking Protection message that protection is currently off for this site -->
<string name="etp_panel_off">Η προστασία είναι ΑΝΕΝΕΡΓΗ για αυτή τη σελίδα</string>
<!-- Header for exceptions list for which sites enhanced tracking protection is always off -->
<string name="enhanced_tracking_protection_exceptions">Η ενισχυμένη προστασία από καταγραφή είναι ανενεργή για αυτή τη σελίδα</string>
<!-- Content description (not visible, for screen readers etc.): Navigate
back from ETP details (Ex: Tracking content) -->
<string name="etp_back_button_content_description">Πλοήγηση προς τα πίσω</string>
<!-- About page Your rights link text -->
<string name="about_your_rights">Τα δικαιώματά σας</string>
@ -1088,6 +1174,8 @@
<string name="preferences_passwords_saved_logins_username">Όνομα χρήστη</string>
<!-- The header for the password for a login -->
<string name="preferences_passwords_saved_logins_password">Κωδικός πρόσβασης</string>
<!-- Message displayed in security prompt to reenter a secret pin to access saved logins -->
<string name="preferences_passwords_saved_logins_enter_pin">Εισάγετε ξανά το PIN σας</string>
<!-- Message displayed when a connection is insecure and we detect the user is entering a password -->
<string name="logins_insecure_connection_warning">Αυτή η σύνδεση δεν είναι ασφαλής. Οι λογαριασμοί που εισάγονται εδώ ενδέχεται να παραβιαστούν.</string>
<!-- Learn more link that will link to a page with more information displayed when a connection is insecure and we detect the user is entering a password -->

View File

@ -1402,7 +1402,7 @@
<string name="search_delete_search_engine_success_message">%s dihapus</string>
<!-- Title text shown for the migration screen to the new browser. Placeholder replaced with app name -->
<string name="migration_title">Selamat datang ke %s yang benar-benar baru</string>
<string name="migration_title">Selamat datang di %s terbaru</string>
<!-- Description text followed by a list of things migrating (e.g. Bookmarks, History). Placeholder replaced with app name-->
<string name="migration_description">Sebuah peramban yang telah didesain ulang sepenuhnya, dengan peningkatan kinerja dan fitur untuk membantu anda dalam melakukan sesuatu secara daring.\n\nHarap tunggu selama kami memperbarui %s dengan milik anda</string>
<!-- Text on the disabled button while in progress. Placeholder replaced with app name -->

View File

@ -36,12 +36,18 @@
<string name="tab_tray_add_new_collection_name">Naam</string>
<!-- Label of button in save to collection dialog for selecting a current collection -->
<string name="tab_tray_select_collection">Collectie selecteren</string>
<!-- Content description for close button while in multiselect mode in tab tray -->
<string name="tab_tray_close_multiselect_content_description">Multiselectiemodus verlaten</string>
<!-- Content description for save to collection button while in multiselect mode in tab tray -->
<string name="tab_tray_collection_button_multiselect_content_description">Geselecteerde tabbladen in collectie opslaan</string>
<!-- Content description for checkmark while tab is selected while in multiselect mode in tab tray. The first parameter is the title of the tab selected -->
<string name="tab_tray_item_selected_multiselect_content_description">%1$s geselecteerd</string>
<!-- Content description when tab is unselected while in multiselect mode in tab tray. The first parameter is the title of the tab unselected -->
<string name="tab_tray_item_unselected_multiselect_content_description">Selectie %1$s ongedaan gemaakt</string>
<!-- Content description announcement when exiting multiselect mode in tab tray -->
<string name="tab_tray_exit_multiselect_content_description">Multiselectiemodus verlaten</string>
<!-- Content description announcement when entering multiselect mode in tab tray -->
<string name="tab_tray_enter_multiselect_content_description">Multiselectiemodus geactiveerd, selecteer tabbladen om in een collectie op te slaan</string>
<!-- Content description on checkmark while tab is selected in multiselect mode in tab tray -->
<string name="tab_tray_multiselect_selected_content_description">Geselecteerd</string>

View File

@ -20,6 +20,7 @@
<string name="pref_key_privacy_link" translatable="false">pref_key_privacy_link</string>
<string name="pref_key_delete_browsing_data" translatable="false">pref_key_delete_browsing_data</string>
<string name="pref_key_delete_browsing_data_on_quit_preference" translatable="false">pref_key_delete_browsing_data_on_quit_preference</string>
<string name="pref_key_notifications" translatable="false">pref_key_notifications</string>
<string name="pref_key_delete_browsing_data_on_quit" translatable="false">pref_key_delete_browsing_data_on_quit</string>
<string name="pref_key_delete_open_tabs_on_quit" translatable="false">pref_key_delete_open_tabs_on_quit</string>
<string name="pref_key_delete_browsing_history_on_quit" translatable="false">pref_key_delete_browsing_history_on_quit</string>
@ -178,6 +179,8 @@
<string name="pref_key_wait_first_paint" translatable="false">pref_key_wait_first_paint</string>
<string name="pref_key_synced_tabs_tabs_tray" translatable="false">pref_key_synced_tabs_tabs_tray</string>
<string name="pref_key_debug_settings" translatable="false">pref_key_debug_settings</string>
<string name="pref_key_open_tabs_count" translatable="false">pref_key_open_tabs_count</string>

View File

@ -36,6 +36,8 @@
<string name="preferences_debug_settings_use_new_search_experience">Use New Search Experience</string>
<!-- Label for the wait until first paint preference -->
<string name="preferences_debug_settings_wait_first_paint">Wait Until First Paint To Show Page Content</string>
<!-- Label for showing Synced Tabs in the tabs tray -->
<string name="preferences_debug_synced_tabs_tabs_tray">Show Synced Tabs in the tabs tray</string>
<!-- Content description (not visible, for screen readers etc.) used to announce [LinkTextView]. -->
<string name="link_text_view_type_announcement" translatable="false">link</string>

View File

@ -300,6 +300,8 @@
<string name="preferences_external_download_manager">External download manager</string>
<!-- Preference for add_ons -->
<string name="preferences_addons">Add-ons</string>
<!-- Preference for notifications -->
<string name="preferences_notifications">Notifications</string>
<!-- Account Preferences -->
<!-- Preference for triggering sync -->
@ -563,6 +565,13 @@
<!-- Text shown when no history exists -->
<string name="history_empty_message">No history here</string>
<!-- Downloads -->
<!-- Text shown when no download exists -->
<string name="download_empty_message">No downloads here</string>
<!-- History multi select title in app bar
The first parameter is the number of downloads selected -->
<string name="download_multi_select_title">%1$d selected</string>
<!-- Crashes -->
<!-- Title text displayed on the tab crash page. This first parameter is the name of the application (For example: Fenix) -->
<string name="tab_crash_title_2">Sorry. %1$s cant load that page.</string>
@ -1446,6 +1455,8 @@
<string name="synced_tabs_sign_in_message">View a list of tabs from your other devices.</string>
<!-- Text displayed on a button in the synced tabs screen to link users to sign in when a user is not signed in to Firefox Sync -->
<string name="synced_tabs_sign_in_button">Sign in to sync</string>
<!-- The text displayed when a synced device has no tabs to show in the list of Synced Tabs. -->
<string name="synced_tabs_no_open_tabs">No open tabs</string>
<!-- Top Sites -->
<!-- Title text displayed in the dialog when top sites limit is reached. -->

View File

@ -43,7 +43,6 @@
<CheckBoxPreference
android:defaultValue="false"
app:isPreferenceVisible="false"
android:key="@string/pref_key_sync_tabs"
android:layout="@layout/checkbox_left_preference"
android:title="@string/preferences_sync_tabs_2"/>

View File

@ -109,6 +109,11 @@
android:key="@string/pref_key_delete_browsing_data_on_quit_preference"
android:title="@string/preferences_delete_browsing_data_on_quit" />
<androidx.preference.Preference
android:icon="@drawable/ic_notifications"
android:key="@string/pref_key_notifications"
android:title="@string/preferences_notifications" />
<androidx.preference.Preference
android:icon="@drawable/ic_data_collection"
android:key="@string/pref_key_data_choices"

View File

@ -14,4 +14,9 @@
android:key="@string/pref_key_wait_first_paint"
android:title="@string/preferences_debug_settings_wait_first_paint"
app:iconSpaceReserved="false" />
<SwitchPreference
android:defaultValue="false"
android:key="@string/pref_key_synced_tabs_tabs_tray"
android:title="@string/preferences_debug_synced_tabs_tabs_tray"
app:iconSpaceReserved="false" />
</PreferenceScreen>

View File

@ -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.library.downloads
import androidx.recyclerview.widget.RecyclerView
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class DownloadAdapterTest {
private lateinit var interactor: DownloadInteractor
private lateinit var adapter: DownloadAdapter
@Before
fun setup() {
interactor = mockk()
adapter = DownloadAdapter(interactor)
every { interactor.select(any()) } just Runs
}
@Test
fun `getItemCount should return the number of tab collections`() {
val download = mockk<DownloadItem>()
assertEquals(0, adapter.itemCount)
adapter.updateDownloads(
downloads = listOf(download)
)
assertEquals(1, adapter.itemCount)
}
@Test
fun `updateData inserts item`() {
val download = mockk<DownloadItem> {
}
val observer = mockk<RecyclerView.AdapterDataObserver>(relaxed = true)
adapter.registerAdapterDataObserver(observer)
adapter.updateDownloads(
downloads = listOf(download)
)
verify { observer.onChanged() }
}
}

View File

@ -0,0 +1,63 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.downloads
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
import org.junit.Assert.assertFalse
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@ExperimentalCoroutinesApi
@RunWith(FenixRobolectricTestRunner::class)
class DownloadControllerTest {
private val downloadItem = DownloadItem(0, "title", "url", "77", "jpg")
private val scope: CoroutineScope = TestCoroutineScope()
private val store: DownloadFragmentStore = mockk(relaxed = true)
private val state: DownloadFragmentState = mockk(relaxed = true)
private val openToFileManager: (DownloadItem, BrowsingMode?) -> Unit = mockk(relaxed = true)
private val invalidateOptionsMenu: () -> Unit = mockk(relaxed = true)
private val controller = DefaultDownloadController(
store,
openToFileManager
)
@Before
fun setUp() {
every { store.state } returns state
}
@Test
fun onPressDownloadItemInNormalMode() {
controller.handleOpen(downloadItem)
verify {
openToFileManager(downloadItem, null)
}
}
@Test
fun onOpenItemInNormalMode() {
controller.handleOpen(downloadItem, BrowsingMode.Normal)
verify {
openToFileManager(downloadItem, BrowsingMode.Normal)
}
}
@Test
fun onBackPressedInNormalMode() {
every { state.mode } returns DownloadFragmentState.Mode.Normal
assertFalse(controller.handleBackPressed())
}
}

View File

@ -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.library.downloads
import io.mockk.every
import io.mockk.mockk
import io.mockk.verifyAll
import org.junit.Assert.assertTrue
import org.junit.Test
class DownloadInteractorTest {
private val downloadItem = DownloadItem(0, "title", "url", "5.6 mb", "png")
val controller: DownloadController = mockk(relaxed = true)
val interactor = DownloadInteractor(controller)
@Test
fun onOpen() {
interactor.open(downloadItem)
verifyAll {
controller.handleOpen(downloadItem)
}
}
@Test
fun onBackPressed() {
every {
controller.handleBackPressed()
} returns true
val backpressHandled = interactor.onBackPressed()
verifyAll {
controller.handleBackPressed()
}
assertTrue(backpressHandled)
}
}

View File

@ -0,0 +1,26 @@
/* 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.sync
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.junit.Test
class ListenerDelegateTest {
@Test
fun `delegate invokes nullable listener`() {
val listener: SyncedTabsView.Listener? = mockk(relaxed = true)
val delegate = ListenerDelegate { listener }
delegate.onRefresh()
verify { listener?.onRefresh() }
delegate.onTabClicked(mockk())
verify { listener?.onTabClicked(any()) }
}
}

View File

@ -11,6 +11,7 @@ import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.browser.storage.sync.Tab
import mozilla.components.browser.storage.sync.TabEntry
import mozilla.components.concept.sync.DeviceType
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
@ -21,7 +22,7 @@ import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class SyncedTabsAdapterTest {
private lateinit var listener: (Tab) -> Unit
private lateinit var listener: SyncedTabsView.Listener
private lateinit var adapter: SyncedTabsAdapter
private val oneTabDevice = SyncedDeviceTabs(
@ -77,10 +78,12 @@ class SyncedTabsAdapterTest {
fun `updateData() adds items for each device and tab`() {
assertEquals(0, adapter.itemCount)
adapter.updateData(listOf(
oneTabDevice,
threeTabDevice
))
adapter.updateData(
listOf(
oneTabDevice,
threeTabDevice
)
)
assertEquals(5, adapter.itemCount)
assertEquals(SyncedTabsViewHolder.DeviceViewHolder.LAYOUT_ID, adapter.getItemViewType(0))

View File

@ -4,16 +4,10 @@
package org.mozilla.fenix.sync
import androidx.navigation.NavController
import io.mockk.mockk
import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mozilla.fenix.R
class SyncedTabsLayoutTest {
@ -25,73 +19,4 @@ class SyncedTabsLayoutTest {
assertFalse(SyncedTabsLayout.pullToRefreshEnableState(ErrorType.SYNC_NEEDS_REAUTHENTICATION))
assertFalse(SyncedTabsLayout.pullToRefreshEnableState(ErrorType.SYNC_UNAVAILABLE))
}
@Test
fun `string resource for error`() {
assertEquals(
R.string.synced_tabs_connect_another_device,
SyncedTabsLayout.stringResourceForError(ErrorType.MULTIPLE_DEVICES_UNAVAILABLE)
)
assertEquals(
R.string.synced_tabs_enable_tab_syncing,
SyncedTabsLayout.stringResourceForError(ErrorType.SYNC_ENGINE_UNAVAILABLE)
)
assertEquals(
R.string.synced_tabs_sign_in_message,
SyncedTabsLayout.stringResourceForError(ErrorType.SYNC_UNAVAILABLE)
)
assertEquals(
R.string.synced_tabs_reauth,
SyncedTabsLayout.stringResourceForError(ErrorType.SYNC_NEEDS_REAUTHENTICATION)
)
assertEquals(
R.string.synced_tabs_no_tabs,
SyncedTabsLayout.stringResourceForError(ErrorType.NO_TABS_AVAILABLE)
)
}
@Test
fun `get error item`() {
val navController = mockk<NavController>()
var errorItem = SyncedTabsLayout.getErrorItem(
navController,
ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
R.string.synced_tabs_connect_another_device
)
assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController)
assertEquals(R.string.synced_tabs_connect_another_device, errorItem.descriptionResId)
errorItem = SyncedTabsLayout.getErrorItem(
navController,
ErrorType.SYNC_ENGINE_UNAVAILABLE,
R.string.synced_tabs_enable_tab_syncing
)
assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController)
assertEquals(R.string.synced_tabs_enable_tab_syncing, errorItem.descriptionResId)
errorItem = SyncedTabsLayout.getErrorItem(
navController,
ErrorType.SYNC_NEEDS_REAUTHENTICATION,
R.string.synced_tabs_reauth
)
assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController)
assertEquals(R.string.synced_tabs_reauth, errorItem.descriptionResId)
errorItem = SyncedTabsLayout.getErrorItem(
navController,
ErrorType.NO_TABS_AVAILABLE,
R.string.synced_tabs_no_tabs
)
assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController)
assertEquals(R.string.synced_tabs_no_tabs, errorItem.descriptionResId)
errorItem = SyncedTabsLayout.getErrorItem(
navController,
ErrorType.SYNC_UNAVAILABLE,
R.string.synced_tabs_sign_in_message
)
assertNotNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController)
assertEquals(R.string.synced_tabs_sign_in_message, errorItem.descriptionResId)
}
}

View File

@ -7,15 +7,18 @@ package org.mozilla.fenix.sync
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import io.mockk.Called
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.android.synthetic.main.sync_tabs_list_item.view.*
import kotlinx.android.synthetic.main.view_synced_tabs_group.view.*
import kotlinx.android.synthetic.main.view_synced_tabs_title.view.*
import mozilla.components.browser.storage.sync.Tab
import mozilla.components.browser.storage.sync.TabEntry
import mozilla.components.concept.sync.Device
import mozilla.components.concept.sync.DeviceType
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
@ -32,6 +35,10 @@ class SyncedTabsViewHolderTest {
private lateinit var deviceViewHolder: SyncedTabsViewHolder.DeviceViewHolder
private lateinit var deviceView: View
private lateinit var deviceViewGroupName: TextView
private lateinit var titleView: View
private lateinit var titleViewHolder: SyncedTabsViewHolder.TitleViewHolder
private lateinit var noTabsView: View
private lateinit var noTabsViewHolder: SyncedTabsViewHolder.NoTabsViewHolder
private val tab = Tab(
history = listOf(
@ -59,6 +66,12 @@ class SyncedTabsViewHolderTest {
every { synced_tabs_group_name } returns deviceViewGroupName
}
deviceViewHolder = SyncedTabsViewHolder.DeviceViewHolder(deviceView)
titleView = inflater.inflate(SyncedTabsViewHolder.TitleViewHolder.LAYOUT_ID, null)
titleViewHolder = SyncedTabsViewHolder.TitleViewHolder(titleView)
noTabsView = inflater.inflate(SyncedTabsViewHolder.NoTabsViewHolder.LAYOUT_ID, null)
noTabsViewHolder = SyncedTabsViewHolder.NoTabsViewHolder(noTabsView)
}
@Test
@ -71,11 +84,11 @@ class SyncedTabsViewHolderTest {
@Test
fun `TabViewHolder calls interactor on click`() {
val interactor = mockk<(Tab) -> Unit>(relaxed = true)
val interactor = mockk<SyncedTabsView.Listener>(relaxed = true)
tabViewHolder.bind(SyncedTabsAdapter.AdapterItem.Tab(tab), interactor)
tabView.performClick()
verify { interactor(tab) }
verify { interactor.onTabClicked(tab) }
}
@Test
@ -109,4 +122,28 @@ class SyncedTabsViewHolderTest {
)
}
}
@Test
fun `TitleViewHolder calls interactor refresh`() {
val interactor = mockk<SyncedTabsView.Listener>(relaxed = true)
titleViewHolder.bind(SyncedTabsAdapter.AdapterItem.Title, interactor)
titleView.findViewById<View>(R.id.refresh_icon).performClick()
verify { interactor.onRefresh() }
}
@Test
fun `NoTabsViewHolder does nothing`() {
val device = mockk<Device> {
every { displayName } returns "Charcoal"
every { deviceType } returns DeviceType.DESKTOP
}
val interactor = mockk<SyncedTabsView.Listener>(relaxed = true)
noTabsViewHolder.bind(SyncedTabsAdapter.AdapterItem.NoTabs(device), interactor)
titleView.performClick()
verify { interactor wasNot Called }
}
}

View File

@ -0,0 +1,72 @@
package org.mozilla.fenix.sync.ext
import org.junit.Test
import androidx.navigation.NavController
import io.mockk.mockk
import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
import org.mozilla.fenix.R
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertEquals
class ErrorTypeKtTest {
@Test
fun `string resource for error`() {
assertEquals(
R.string.synced_tabs_connect_another_device,
ErrorType.MULTIPLE_DEVICES_UNAVAILABLE.toStringRes()
)
assertEquals(
R.string.synced_tabs_enable_tab_syncing,
ErrorType.SYNC_ENGINE_UNAVAILABLE.toStringRes()
)
assertEquals(
R.string.synced_tabs_sign_in_message,
ErrorType.SYNC_UNAVAILABLE.toStringRes()
)
assertEquals(
R.string.synced_tabs_reauth,
ErrorType.SYNC_NEEDS_REAUTHENTICATION.toStringRes()
)
assertEquals(
R.string.synced_tabs_no_tabs,
ErrorType.NO_TABS_AVAILABLE.toStringRes()
)
}
@Test
fun `get error item`() {
val navController = mockk<NavController>()
var errorItem = ErrorType.MULTIPLE_DEVICES_UNAVAILABLE.toAdapterItem(
R.string.synced_tabs_connect_another_device, navController
)
assertNull(errorItem.navController)
assertEquals(R.string.synced_tabs_connect_another_device, errorItem.descriptionResId)
errorItem = ErrorType.SYNC_ENGINE_UNAVAILABLE.toAdapterItem(
R.string.synced_tabs_enable_tab_syncing, navController
)
assertNull(errorItem.navController)
assertEquals(R.string.synced_tabs_enable_tab_syncing, errorItem.descriptionResId)
errorItem = ErrorType.SYNC_NEEDS_REAUTHENTICATION.toAdapterItem(
R.string.synced_tabs_reauth, navController
)
assertNull(errorItem.navController)
assertEquals(R.string.synced_tabs_reauth, errorItem.descriptionResId)
errorItem = ErrorType.NO_TABS_AVAILABLE.toAdapterItem(
R.string.synced_tabs_no_tabs, navController
)
assertNull(errorItem.navController)
assertEquals(R.string.synced_tabs_no_tabs, errorItem.descriptionResId)
errorItem = ErrorType.SYNC_UNAVAILABLE.toAdapterItem(
R.string.synced_tabs_sign_in_message, navController
)
assertNotNull(errorItem.navController)
assertEquals(R.string.synced_tabs_sign_in_message, errorItem.descriptionResId)
}
}

View File

@ -0,0 +1,94 @@
/* 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.sync.ext
import io.mockk.every
import io.mockk.mockk
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.browser.storage.sync.Tab
import mozilla.components.browser.storage.sync.TabEntry
import mozilla.components.concept.sync.DeviceType
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mozilla.fenix.sync.SyncedTabsAdapter
class SyncedTabsAdapterKtTest {
private val noTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Charcoal"
every { deviceType } returns DeviceType.DESKTOP
},
tabs = emptyList()
)
private val oneTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Charcoal"
every { deviceType } returns DeviceType.DESKTOP
},
tabs = listOf(Tab(
history = listOf(TabEntry(
title = "Mozilla",
url = "https://mozilla.org",
iconUrl = null
)),
active = 0,
lastUsed = 0L
))
)
private val twoTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Emerald"
every { deviceType } returns DeviceType.MOBILE
},
tabs = listOf(
Tab(
history = listOf(TabEntry(
title = "Mozilla",
url = "https://mozilla.org",
iconUrl = null
)),
active = 0,
lastUsed = 0L
),
Tab(
history = listOf(
TabEntry(
title = "Firefox",
url = "https://firefox.com",
iconUrl = null
)
),
active = 0,
lastUsed = 0L
)
)
)
@Test
fun `verify ordering of adapter items`() {
val syncedDeviceList = listOf(oneTabDevice, twoTabDevice)
val adapterData = syncedDeviceList.toAdapterList()
assertEquals(5, adapterData.count())
assertTrue(adapterData[0] is SyncedTabsAdapter.AdapterItem.Device)
assertTrue(adapterData[1] is SyncedTabsAdapter.AdapterItem.Tab)
assertTrue(adapterData[2] is SyncedTabsAdapter.AdapterItem.Device)
assertTrue(adapterData[3] is SyncedTabsAdapter.AdapterItem.Tab)
assertTrue(adapterData[4] is SyncedTabsAdapter.AdapterItem.Tab)
}
@Test
fun `verify no tabs displayed`() {
val syncedDeviceList = listOf(noTabDevice)
val adapterData = syncedDeviceList.toAdapterList()
assertEquals(2, adapterData.count())
assertTrue(adapterData[0] is SyncedTabsAdapter.AdapterItem.Device)
assertTrue(adapterData[1] is SyncedTabsAdapter.AdapterItem.NoTabs)
}
}

View File

@ -26,6 +26,8 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
@ -34,6 +36,7 @@ import org.mozilla.fenix.ext.sessionsOfType
@OptIn(ExperimentalCoroutinesApi::class)
class DefaultTabTrayControllerTest {
private val activity: HomeActivity = mockk(relaxed = true)
private val profiler: Profiler? = mockk(relaxed = true)
private val navController: NavController = mockk()
private val sessionManager: SessionManager = mockk(relaxed = true)
@ -81,6 +84,7 @@ class DefaultTabTrayControllerTest {
every { tabCollection.title } returns "Collection title"
controller = DefaultTabTrayController(
activity = activity,
profiler = profiler,
sessionManager = sessionManager,
browsingModeManager = browsingModeManager,
@ -156,6 +160,15 @@ class DefaultTabTrayControllerTest {
}
}
@Test
fun onSyncedTabClicked() {
controller.onSyncedTabClicked(mockk(relaxed = true))
verify {
activity.openToBrowserAndLoad(any(), true, BrowserDirection.FromTabTray)
}
}
@Test
fun handleBackPressed() {
every { tabTrayFragmentStore.state.mode } returns TabTrayDialogFragmentState.Mode.MultiSelect(

View File

@ -0,0 +1,133 @@
package org.mozilla.fenix.tabtray
import android.view.LayoutInflater
import android.view.View
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.recyclerview.widget.ConcatAdapter
import io.mockk.Called
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.sync.SyncedTabsViewHolder
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentAction.EnterMultiSelectMode
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentAction.ExitMultiSelectMode
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentState.Mode
@ExperimentalCoroutinesApi
@RunWith(FenixRobolectricTestRunner::class)
class SyncedTabsControllerTest {
private val testDispatcher = TestCoroutineDispatcher()
@get:Rule
val coroutinesTestRule = MainCoroutineRule(testDispatcher)
private lateinit var view: View
private lateinit var controller: SyncedTabsController
private lateinit var lifecycleOwner: LifecycleOwner
private lateinit var lifecycle: LifecycleRegistry
private lateinit var concatAdapter: ConcatAdapter
private lateinit var store: TabTrayDialogFragmentStore
@Before
fun setup() = runBlockingTest {
lifecycleOwner = mockk()
lifecycle = LifecycleRegistry(lifecycleOwner)
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START)
every { lifecycleOwner.lifecycle } returns lifecycle
concatAdapter = mockk()
every { concatAdapter.addAdapter(any(), any()) } returns true
every { concatAdapter.removeAdapter(any()) } returns true
store = TabTrayDialogFragmentStore(
initialState = TabTrayDialogFragmentState(
mode = Mode.Normal,
browserState = mockk(relaxed = true)
)
)
view = LayoutInflater.from(testContext).inflate(R.layout.about_list_item, null)
controller =
SyncedTabsController(lifecycleOwner, view, store, concatAdapter, coroutineContext)
}
@Test
fun `display synced tabs in reverse`() {
val tabs = listOf(
SyncedDeviceTabs(
device = mockk(relaxed = true),
tabs = listOf(
mockk(relaxed = true),
mockk(relaxed = true)
)
)
)
controller.displaySyncedTabs(tabs)
val itemCount = controller.adapter.itemCount
// title + device name + 2 tabs
assertEquals(4, itemCount)
assertEquals(
SyncedTabsViewHolder.TitleViewHolder.LAYOUT_ID,
controller.adapter.getItemViewType(itemCount - 1)
)
assertEquals(
SyncedTabsViewHolder.DeviceViewHolder.LAYOUT_ID,
controller.adapter.getItemViewType(itemCount - 2)
)
assertEquals(
SyncedTabsViewHolder.TabViewHolder.LAYOUT_ID,
controller.adapter.getItemViewType(itemCount - 3)
)
assertEquals(
SyncedTabsViewHolder.TabViewHolder.LAYOUT_ID,
controller.adapter.getItemViewType(itemCount - 4)
)
}
@Test
fun `show error when we go kaput`() {
controller.onError(ErrorType.SYNC_NEEDS_REAUTHENTICATION)
assertEquals(1, controller.adapter.itemCount)
assertEquals(
SyncedTabsViewHolder.ErrorViewHolder.LAYOUT_ID,
controller.adapter.getItemViewType(0)
)
}
@Test
fun `do nothing on init, drop first event`() {
verify { concatAdapter wasNot Called }
}
@Test
fun `concatAdapter updated on mode changes`() = testDispatcher.runBlockingTest {
store.dispatch(EnterMultiSelectMode).joinBlocking()
verify { concatAdapter.removeAdapter(any()) }
store.dispatch(ExitMultiSelectMode).joinBlocking()
verify { concatAdapter.addAdapter(0, any()) }
}
}

View File

@ -53,6 +53,12 @@ class TabTrayFragmentInteractorTest {
verify { controller.onCloseAllTabsClicked(true) }
}
@Test
fun onSyncedTabClicked() {
interactor.onSyncedTabClicked(mockk(relaxed = true))
verify { controller.onSyncedTabClicked(any()) }
}
@Test
fun onBackPressed() {
interactor.onBackPressed()

File diff suppressed because one or more lines are too long