Copione merged onto master
commit
bd1d5c94be
26
README.md
26
README.md
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>?
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ interface ToolbarMenu {
|
|||
object ReaderModeAppearance : Item()
|
||||
object Bookmarks : Item()
|
||||
object History : Item()
|
||||
object Downloads : Item()
|
||||
}
|
||||
|
||||
val menuBuilder: BrowserMenuBuilder
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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>
|
|
@ -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>
|
|
@ -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" />
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 can’t 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. -->
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()) }
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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()) }
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
486
docs/metrics.md
486
docs/metrics.md
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue