1
0
Fork 0

Copione merged onto master

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

View File

@ -105,21 +105,23 @@ you want these variants to be:
#### Performance Build Variants #### Performance Build Variants
For accurate performance measurements, read this section! 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): 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!
- 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
If you want to run **performance tests/benchmarks** in automation or locally: 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)
- 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 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 ## Pre-push hooks
To reduce review turn-around time, we'd like all pushes to run tests locally. We'd To reduce review turn-around time, we'd like all pushes to run tests locally. We'd

View File

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

View File

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

View File

@ -222,7 +222,7 @@ private fun assertLibrariesUsed() {
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.perform(click()) .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() Espresso.pressBack()
} }

View File

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

View File

@ -20,14 +20,11 @@ object FeatureFlags {
const val loginsEdit = true 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 val syncedTabsInTabsTray = Config.channel.isNightlyOrDebug
/**
* Enables new tab tray pref
*/
val tabTray = Config.channel.isNightlyOrDebug
/** /**
* Enables viewing tab history * Enables viewing tab history
@ -48,4 +45,9 @@ object FeatureFlags {
* Enables downloads with external download managers. * Enables downloads with external download managers.
*/ */
val externalDownloadManager = Config.channel.isNightlyOrDebug val externalDownloadManager = Config.channel.isNightlyOrDebug
/**
* Enables viewing downloads in browser.
*/
val viewDownloads = Config.channel.isNightlyOrDebug
} }

View File

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

View File

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

View File

@ -488,7 +488,7 @@ sealed class Event {
NEW_PRIVATE_TAB, SHARE, BACK, FORWARD, RELOAD, STOP, OPEN_IN_FENIX, 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, 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, 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>? override val extras: Map<Events.browserMenuActionKeys, String>?

View File

@ -380,6 +380,13 @@ class DefaultBrowserToolbarController(
BrowserFragmentDirections.actionGlobalHistoryFragment() 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.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER
ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS
ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY
ToolbarMenu.Item.Downloads -> Event.BrowserMenuItemTapped.Item.DOWNLOADS
} }
activity.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem)) activity.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -270,6 +270,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
resources.getString(R.string.pref_key_delete_browsing_data_on_quit_preference) -> { resources.getString(R.string.pref_key_delete_browsing_data_on_quit_preference) -> {
SettingsFragmentDirections.actionSettingsFragmentToDeleteBrowsingDataOnQuitFragment() 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) -> { resources.getString(R.string.pref_key_customize) -> {
SettingsFragmentDirections.actionSettingsFragmentToCustomizationFragment() SettingsFragmentDirections.actionSettingsFragmentToCustomizationFragment()
} }
@ -352,6 +359,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
findPreference<Preference>( findPreference<Preference>(
getPreferenceKey(R.string.pref_key_debug_settings) getPreferenceKey(R.string.pref_key_debug_settings)
)?.isVisible = requireContext().settings().showSecretDebugMenuThisSession )?.isVisible = requireContext().settings().showSecretDebugMenuThisSession
findPreference<Preference>(
getPreferenceKey(R.string.pref_key_notifications)
)?.isVisible = requireContext().settings().showNotificationsSetting
} }
private fun getClickListenerForMakeDefaultBrowser(): Preference.OnPreferenceClickListener { private fun getClickListenerForMakeDefaultBrowser(): Preference.OnPreferenceClickListener {

View File

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

View File

@ -10,14 +10,18 @@ import androidx.navigation.NavController
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import mozilla.components.browser.storage.sync.SyncedDeviceTabs 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.DeviceViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.ErrorViewHolder 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.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.browser.storage.sync.Tab as SyncTab
import mozilla.components.concept.sync.Device as SyncDevice import mozilla.components.concept.sync.Device as SyncDevice
class SyncedTabsAdapter( class SyncedTabsAdapter(
private val listener: (SyncTab) -> Unit private val newListener: SyncedTabsView.Listener
) : ListAdapter<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(DiffCallback) { ) : ListAdapter<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(DiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder {
@ -27,30 +31,26 @@ class SyncedTabsAdapter(
DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView) DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView)
TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView) TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView)
ErrorViewHolder.LAYOUT_ID -> ErrorViewHolder(itemView) ErrorViewHolder.LAYOUT_ID -> ErrorViewHolder(itemView)
TitleViewHolder.LAYOUT_ID -> TitleViewHolder(itemView)
NoTabsViewHolder.LAYOUT_ID -> NoTabsViewHolder(itemView)
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }
override fun onBindViewHolder(holder: SyncedTabsViewHolder, position: Int) { 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)) { override fun getItemViewType(position: Int) = when (getItem(position)) {
is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID
is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID
is AdapterItem.Error -> ErrorViewHolder.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>) { fun updateData(syncedTabs: List<SyncedDeviceTabs>) {
val allDeviceTabs = mutableListOf<AdapterItem>() val allDeviceTabs = syncedTabs.toAdapterList()
syncedTabs.forEach { (device, tabs) ->
if (tabs.isNotEmpty()) {
allDeviceTabs.add(AdapterItem.Device(device))
tabs.mapTo(allDeviceTabs) { AdapterItem.Tab(it) }
}
}
submitList(allDeviceTabs) submitList(allDeviceTabs)
} }
@ -59,7 +59,11 @@ class SyncedTabsAdapter(
when (oldItem) { when (oldItem) {
is AdapterItem.Device -> is AdapterItem.Device ->
newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id 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 oldItem == newItem
} }
@ -68,9 +72,35 @@ class SyncedTabsAdapter(
oldItem == newItem oldItem == newItem
} }
/**
* The various types of adapter items that can be found in a [SyncedTabsAdapter].
*/
sealed class AdapterItem { 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() data class Device(val device: SyncDevice) : AdapterItem()
/**
* A tab that was synced.
*/
data class Tab(val tab: SyncTab) : AdapterItem() 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( data class Error(
val descriptionResId: Int, val descriptionResId: Int,
val navController: NavController? = null val navController: NavController? = null

View File

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

View File

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

View File

@ -7,29 +7,36 @@ package org.mozilla.fenix.sync
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.sync_tabs_error_row.view.* 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.sync_tabs_list_item.view.*
import kotlinx.android.synthetic.main.view_synced_tabs_group.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.concept.sync.DeviceType
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.support.ktx.android.util.dpToPx import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem 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) { 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) { 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) bindTab(item as AdapterItem.Tab)
itemView.setOnClickListener { 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) { 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 val errorItem = item as AdapterItem.Error
setErrorMargins() setErrorMargins()
@ -69,7 +76,7 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
class DeviceViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { 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) 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() { internal fun setErrorMargins() {
val lp = LinearLayout.LayoutParams( val lp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT,

View File

@ -0,0 +1,38 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.sync.ext
import androidx.annotation.StringRes
import androidx.navigation.NavController
import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
import org.mozilla.fenix.R
import org.mozilla.fenix.sync.SyncedTabsAdapter
/**
* Converts the error type to the appropriate matching string resource for displaying to the user.
*/
fun ErrorType.toStringRes() = when (this) {
ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device
ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing
ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_sign_in_message
ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth
ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs
}
/**
* Converts an error type to an [SyncedTabsAdapter.AdapterItem.Error].
*/
fun ErrorType.toAdapterItem(
@StringRes stringResId: Int,
navController: NavController? = null
) = when (this) {
ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
ErrorType.SYNC_ENGINE_UNAVAILABLE,
ErrorType.SYNC_NEEDS_REAUTHENTICATION,
ErrorType.NO_TABS_AVAILABLE -> SyncedTabsAdapter.AdapterItem
.Error(descriptionResId = stringResId)
ErrorType.SYNC_UNAVAILABLE -> SyncedTabsAdapter.AdapterItem
.Error(descriptionResId = stringResId, navController = navController)
}

View File

@ -0,0 +1,22 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.sync.ext
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem
/**
* Converts a list of [SyncedDeviceTabs] into a list of [AdapterItem].
*/
fun List<SyncedDeviceTabs>.toAdapterList() = asSequence().flatMap { (device, tabs) ->
val deviceTabs = if (tabs.isEmpty()) {
sequenceOf(AdapterItem.NoTabs(device))
} else {
tabs.asSequence().map { AdapterItem.Tab(it) }
}
sequenceOf(AdapterItem.Device(device)) + deviceTabs
}.toList()

View File

@ -0,0 +1,85 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.tabtray
import android.view.View
import androidx.fragment.app.FragmentManager.findFragment
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ConcatAdapter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.sync.ListenerDelegate
import org.mozilla.fenix.sync.SyncedTabsAdapter
import org.mozilla.fenix.sync.ext.toAdapterList
import org.mozilla.fenix.sync.ext.toAdapterItem
import org.mozilla.fenix.sync.ext.toStringRes
import kotlin.coroutines.CoroutineContext
@OptIn(ExperimentalCoroutinesApi::class)
class SyncedTabsController(
lifecycleOwner: LifecycleOwner,
private val view: View,
store: TabTrayDialogFragmentStore,
private val concatAdapter: ConcatAdapter,
coroutineContext: CoroutineContext = Dispatchers.Main
) : SyncedTabsView {
override var listener: SyncedTabsView.Listener? = null
val adapter = SyncedTabsAdapter(ListenerDelegate { listener })
private val scope: CoroutineScope = CoroutineScope(coroutineContext)
init {
store.flowScoped(lifecycleOwner) { flow ->
flow.map { it.mode }
.ifChanged()
.drop(1)
.collect { mode ->
when (mode) {
is TabTrayDialogFragmentState.Mode.Normal -> {
concatAdapter.addAdapter(0, adapter)
}
is TabTrayDialogFragmentState.Mode.MultiSelect -> {
concatAdapter.removeAdapter(adapter)
}
}
}
}
}
override fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>) {
scope.launch {
val tabsList = listOf(SyncedTabsAdapter.AdapterItem.Title) + syncedTabs.toAdapterList()
// Reverse layout for TabTrayView which does things backwards.
adapter.submitList(tabsList.reversed())
}
}
override fun onError(error: SyncedTabsView.ErrorType) {
scope.launch {
val navController: NavController? = try {
findFragment<TabTrayDialogFragment>(view).findNavController()
} catch (exception: IllegalStateException) {
null
}
val descriptionResId = error.toStringRes()
val errorItem = error.toAdapterItem(descriptionResId, navController)
adapter.submitList(listOf(errorItem))
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -70,6 +70,9 @@
<action <action
android:id="@+id/action_global_historyFragment" android:id="@+id/action_global_historyFragment"
app:destination="@id/historyFragment" /> app:destination="@id/historyFragment" />
<action android:id="@+id/action_global_downloadsFragment"
app:destination="@id/downloadsFragment" />
<action <action
android:id="@+id/action_global_accountProblemFragment" android:id="@+id/action_global_accountProblemFragment"
app:destination="@id/accountProblemFragment" /> app:destination="@id/accountProblemFragment" />
@ -239,6 +242,12 @@
android:label="@string/library_history" android:label="@string/library_history"
tools:layout="@layout/fragment_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 <fragment
android:id="@+id/bookmarkFragment" android:id="@+id/bookmarkFragment"
android:name="org.mozilla.fenix.library.bookmarks.BookmarkFragment" android:name="org.mozilla.fenix.library.bookmarks.BookmarkFragment"

View File

@ -37,6 +37,8 @@
<!-- Content description for checkmark while tab is selected while in multiselect mode in tab tray. The first parameter is the title of the tab selected --> <!-- 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> <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 --> <!-- 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> <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 --> <!-- Content description announcement when entering multiselect mode in tab tray -->
@ -164,8 +166,8 @@
<!-- Search Fragment --> <!-- Search Fragment -->
<!-- Button in the search view that lets a user search by scanning a QR code --> <!-- Button in the search view that lets a user search by scanning a QR code -->
<string name="search_scan_button">Skeniraj</string> <string name="search_scan_button">Skeniraj</string>
<!-- Button in the search view that lets a user search by using a shortcut --> <!-- Button in the search view that lets a user change their search engine -->
<string name="search_engines_shortcut_button">Pretraživač</string> <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 --> <!-- 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> <string name="search_shortcuts_engine_settings">Postavke pretraživača</string>
<!-- Header displayed when selecting a shortcut search engine --> <!-- Header displayed when selecting a shortcut search engine -->
@ -294,6 +296,8 @@
<string name="preferences_account_settings">Postavke računa</string> <string name="preferences_account_settings">Postavke računa</string>
<!-- Preference for open links in third party apps --> <!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Otvori linkove u aplikacijama</string> <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 --> <!-- Preference for add_ons -->
<string name="preferences_addons">Add-oni</string> <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> <string name="saved_login_duplicate">Prijava sa tim korisničkim imenom već postoji</string>
<!-- Synced Tabs --> <!-- Synced Tabs -->
<!-- Text displayed when user is not logged into a Firefox Account --> <!-- Text displayed to ask user to connect another device as no devices found with 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 -->
<string name="synced_tabs_connect_another_device">Povežite drugi uređaj</string> <string name="synced_tabs_connect_another_device">Povežite drugi uređaj</string>
<!-- Text displayed asking user to re-authenticate --> <!-- Text displayed asking user to re-authenticate -->
<string name="synced_tabs_reauth">Ponovo potvrdite identitet.</string> <string name="synced_tabs_reauth">Ponovo potvrdite identitet.</string>
@ -1465,13 +1467,4 @@
<!-- Confirmation dialog button text when top sites limit is reached. --> <!-- Confirmation dialog button text when top sites limit is reached. -->
<string name="top_sites_max_limit_confirmation_button">OK, razumijem</string> <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> </resources>

View File

@ -34,6 +34,11 @@
<!-- Label of button in save to collection dialog for selecting a current collection --> <!-- Label of button in save to collection dialog for selecting a current collection -->
<string name="tab_tray_select_collection">Επιλογή συλλογής</string> <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) --> <!-- About content. The first parameter is the name of the application. (For example: Fenix) -->
<string name="about_content">Το %1$s αναπτύσσεται από τη Mozilla.</string> <string name="about_content">Το %1$s αναπτύσσεται από τη Mozilla.</string>
@ -255,6 +260,8 @@
<string name="developer_tools_category">Εργαλεία προγραμματιστή</string> <string name="developer_tools_category">Εργαλεία προγραμματιστή</string>
<!-- Preference for developers --> <!-- Preference for developers -->
<string name="preferences_remote_debugging">Απομακρυσμένος εντοπισμός σφαλμάτων μέσω USB</string> <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 --> <!-- Preference title for switch preference to show search suggestions -->
<string name="preferences_show_search_suggestions">Εμφάνιση προτάσεων αναζήτησης</string> <string name="preferences_show_search_suggestions">Εμφάνιση προτάσεων αναζήτησης</string>
<!-- Preference title for switch preference to show voice search button --> <!-- Preference title for switch preference to show voice search button -->
@ -271,6 +278,8 @@
<string name="preferences_account_settings">Ρυθμίσεις λογαριασμού</string> <string name="preferences_account_settings">Ρυθμίσεις λογαριασμού</string>
<!-- Preference for open links in third party apps --> <!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Άνοιγμα συνδέσμων σε εφαρμογές</string> <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 --> <!-- Preference for add_ons -->
<string name="preferences_addons">Πρόσθετα</string> <string name="preferences_addons">Πρόσθετα</string>
@ -322,17 +331,25 @@
<string name="preferences_tracking_protection">Προστασία από καταγραφή</string> <string name="preferences_tracking_protection">Προστασία από καταγραφή</string>
<!-- Preference for tracking protection exceptions --> <!-- Preference for tracking protection exceptions -->
<string name="preferences_tracking_protection_exceptions">Εξαιρέσεις</string> <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 --> <!-- 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> <string name="exceptions_empty_message_learn_more_link">Μάθετε περισσότερα</string>
<!-- Preference switch for Telemetry --> <!-- Preference switch for Telemetry -->
<string name="preferences_telemetry">Τηλεμετρία</string> <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 --> <!-- Preference switch for marketing data collection -->
<string name="preferences_marketing_data">Δεδομένα μάρκετινγκ</string> <string name="preferences_marketing_data">Δεδομένα μάρκετινγκ</string>
<!-- Title for experiments preferences --> <!-- Title for experiments preferences -->
<string name="preference_experiments">Πειράματα</string> <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 --> <!-- Preference switch for Mozilla location service -->
<string name="preferences_mozilla_location_service">Υπηρεσία τοποθεσίας Mozilla</string> <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) --> <!-- 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> <string name="tracking_protection_off">Ανενεργό</string>
<!-- Label that indicates that all video and audio autoplay is allowed --> <!-- Label that indicates that all video and audio autoplay is allowed -->
<string name="preference_option_autoplay_allowed2">Αποδοχή ήχου και βίντεο</string> <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 --> <!-- Label that indicates that video autoplay is allowed, but audio autoplay is blocked -->
<string name="preference_option_autoplay_block_audio2">Φραγή ήχου μόνο</string> <string name="preference_option_autoplay_block_audio2">Φραγή ήχου μόνο</string>
<!-- Label that indicates that all video and audio autoplay is blocked --> <!-- Label that indicates that all video and audio autoplay is blocked -->
@ -661,6 +680,8 @@
<string name="collection_menu_button_content_description">Μενού συλλογής</string> <string name="collection_menu_button_content_description">Μενού συλλογής</string>
<!-- No Open Tabs Message Header --> <!-- No Open Tabs Message Header -->
<string name="no_collections_header1">Συλλέξτε όλα όσα έχουν σημασία για εσάς</string> <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 --> <!-- Title for the "select tabs" step of the collection creator -->
<string name="create_collection_select_tabs">Επιλέξτε καρτέλες</string> <string name="create_collection_select_tabs">Επιλέξτε καρτέλες</string>
<!-- Title for the "select collection" step of the collection creator --> <!-- 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. <!-- 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. --> %d is a placeholder for the number of tabs selected. -->
<string name="create_collection_save_to_collection_tab_selected">Επιλέχθηκε %d καρτέλα</string> <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 --> <!-- Content description (not visible, for screen readers etc.): button to close the collection creator -->
<string name="create_collection_close">Κλείσιμο</string> <string name="create_collection_close">Κλείσιμο</string>
<!-- Button to save currently selected tabs in the "select tabs" step of the collection creator--> <!-- Button to save currently selected tabs in the "select tabs" step of the collection creator-->
@ -718,8 +745,12 @@
<string name="sync_offline">Εκτός σύνδεσης</string> <string name="sync_offline">Εκτός σύνδεσης</string>
<!-- An option to connect additional devices --> <!-- An option to connect additional devices -->
<string name="sync_connect_device">Σύνδεση άλλης συσκευής</string> <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 --> <!-- Confirmation dialog button -->
<string name="sync_confirmation_button">Το κατάλαβα</string> <string name="sync_confirmation_button">Το κατάλαβα</string>
<!-- Share error message -->
<string name="share_error_snackbar">Αδυναμία κοινοποίησης σε αυτή την εφαρμογή</string>
<!-- Add new device screen title --> <!-- Add new device screen title -->
<string name="sync_add_new_device_title">Αποστολή σε συσκευή</string> <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 --> <!-- 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> <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 --> <!-- 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> <string name="tip_firefox_preview_moved_button_2">Λήψη του Firefox για Android Beta</string>
<!-- text for firefox preview moving tip header. "Firefox Nightly" is intentionally hardcoded --> <!-- text for firefox preview moving tip header. "Firefox Nightly" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_header_preview_installed">Το Firefox Nightly έχει μετακινηθεί</string> <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 --> <!-- text for firefox preview moving tip button -->
<string name="tip_firefox_preview_moved_button_preview_installed">Εναλλαγή στο νέο Nightly</string> <string name="tip_firefox_preview_moved_button_preview_installed">Εναλλαγή στο νέο Nightly</string>
<!-- text for firefox preview moving tip header. "Firefox Nightly" is intentionally hardcoded --> <!-- 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> <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 --> <!-- text for firefox preview moving tip button -->
<string name="tip_firefox_preview_moved_button_preview_not_installed">Αποκτήστε το νέο Nightly</string> <string name="tip_firefox_preview_moved_button_preview_not_installed">Αποκτήστε το νέο Nightly</string>
@ -872,6 +915,11 @@
<string name="onboarding_feature_section_header">Γνωρίστε το %s</string> <string name="onboarding_feature_section_header">Γνωρίστε το %s</string>
<!-- text for the "What's New" onboarding card header --> <!-- text for the "What's New" onboarding card header -->
<string name="onboarding_whats_new_header1">Δείτε τι νέο υπάρχει</string> <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 <!-- text for the firefox account onboarding card header
The first parameter is the name of the app (e.g. Firefox Preview) --> The first parameter is the name of the app (e.g. Firefox Preview) -->
<string name="onboarding_firefox_account_header">Αξιοποιήστε στο έπακρο το %s.</string> <string name="onboarding_firefox_account_header">Αξιοποιήστε στο έπακρο το %s.</string>
@ -888,6 +936,9 @@
<!-- text for the tracking protection onboarding card header --> <!-- text for the tracking protection onboarding card header -->
<string name="onboarding_tracking_protection_header_2">Αυτόματο απόρρητο</string> <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 --> <!-- text for tracking protection radio button option for standard level of blocking -->
<string name="onboarding_tracking_protection_standard_button_2">Τυπική (προεπιλογή)</string> <string name="onboarding_tracking_protection_standard_button_2">Τυπική (προεπιλογή)</string>
<!-- text for tracking protection radio button option for strict level of blocking --> <!-- text for tracking protection radio button option for strict level of blocking -->
@ -900,6 +951,8 @@
<string name="onboarding_private_browsing_button">Άνοιγμα ρυθμίσεων</string> <string name="onboarding_private_browsing_button">Άνοιγμα ρυθμίσεων</string>
<!-- text for the privacy notice onboarding card header --> <!-- text for the privacy notice onboarding card header -->
<string name="onboarding_privacy_notice_header">Το απόρρητό σας</string> <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 --> <!-- Content description (not visible, for screen readers etc.): Close onboarding screen -->
<string name="onboarding_close">Κλείσιμο</string> <string name="onboarding_close">Κλείσιμο</string>
@ -918,6 +971,10 @@
<!-- Theme setting for light mode --> <!-- Theme setting for light mode -->
<string name="onboarding_theme_light_title">Φωτεινό θέμα</string> <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 --> <!-- Text shown in snackbar when sharing tabs failed -->
<string name="sync_sent_tab_error_snackbar">Δεν ήταν δυνατή η αποστολή</string> <string name="sync_sent_tab_error_snackbar">Δεν ήταν δυνατή η αποστολή</string>
<!-- Text shown in snackbar for the "retry" action that the user has after sharing tabs failed --> <!-- 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 --> <!-- Text shown for settings option for sign with email -->
<string name="sign_in_with_email">Χρήση email</string> <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 --> <!-- Option to continue signing out of account shown in confirmation dialog to sign out of account -->
<string name="sign_out_disconnect">Αποσύνδεση</string> <string name="sign_out_disconnect">Αποσύνδεση</string>
<!-- Option to cancel signing out shown in confirmation dialog to sign out of account --> <!-- Option to cancel signing out shown in confirmation dialog to sign out of account -->
<string name="sign_out_cancel">Ακύρωση</string> <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 --> <!-- Enhanced Tracking Protection -->
<!-- Link displayed in enhanced tracking protection panel to access tracking protection settings --> <!-- Link displayed in enhanced tracking protection panel to access tracking protection settings -->
<string name="etp_settings">Ρυθμίσεις προστασίας</string> <string name="etp_settings">Ρυθμίσεις προστασίας</string>
@ -951,12 +1015,22 @@
<string name="preference_enhanced_tracking_protection_explanation_learn_more">Μάθετε περισσότερα</string> <string name="preference_enhanced_tracking_protection_explanation_learn_more">Μάθετε περισσότερα</string>
<!-- Preference for enhanced tracking protection for the standard protection settings --> <!-- Preference for enhanced tracking protection for the standard protection settings -->
<string name="preference_enhanced_tracking_protection_standard_default_1">Τυπική (προεπιλογή)</string> <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 --> <!-- Preference for enhanced tracking protection for the strict protection settings -->
<string name="preference_enhanced_tracking_protection_strict">Αυστηρή</string> <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 --> <!-- Preference for enhanced tracking protection for the custom protection settings -->
<string name="preference_enhanced_tracking_protection_custom">Προσαρμοσμένη</string> <string name="preference_enhanced_tracking_protection_custom">Προσαρμοσμένη</string>
<!-- Preference description for enhanced tracking protection for the strict protection settings --> <!-- Preference description for enhanced tracking protection for the strict protection settings -->
<string name="preference_enhanced_tracking_protection_custom_description_2">Επιλέξτε ιχνηλάτες και σενάρια για αποκλεισμό.</string> <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 --> <!-- 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--> <!-- Preference for enhanced tracking protection for the custom protection settings for cookies-->
<string name="preference_enhanced_tracking_protection_custom_cookies">Cookies</string> <string name="preference_enhanced_tracking_protection_custom_cookies">Cookies</string>
@ -980,6 +1054,9 @@
<string name="preference_enhanced_tracking_protection_custom_cryptominers">Cryptominers</string> <string name="preference_enhanced_tracking_protection_custom_cryptominers">Cryptominers</string>
<!-- Preference for enhanced tracking protection for the custom protection settings --> <!-- Preference for enhanced tracking protection for the custom protection settings -->
<string name="preference_enhanced_tracking_protection_custom_fingerprinters">Fingerprinters</string> <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 --> <!-- Category of trackers (social media trackers) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_social_media_trackers_title">Ιχνηλάτες κοινωνικών δικτύων</string> <string name="etp_social_media_trackers_title">Ιχνηλάτες κοινωνικών δικτύων</string>
<!-- Category of trackers (cross-site tracking cookies) that can be blocked by Enhanced Tracking Protection --> <!-- 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> <string name="etp_fingerprinters_title">Fingerprinters</string>
<!-- Category of trackers (tracking content) that can be blocked by Enhanced Tracking Protection --> <!-- Category of trackers (tracking content) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_tracking_content_title">Περιεχόμενο καταγραφής</string> <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 --> <!-- About page Your rights link text -->
<string name="about_your_rights">Τα δικαιώματά σας</string> <string name="about_your_rights">Τα δικαιώματά σας</string>
@ -1088,6 +1174,8 @@
<string name="preferences_passwords_saved_logins_username">Όνομα χρήστη</string> <string name="preferences_passwords_saved_logins_username">Όνομα χρήστη</string>
<!-- The header for the password for a login --> <!-- The header for the password for a login -->
<string name="preferences_passwords_saved_logins_password">Κωδικός πρόσβασης</string> <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 --> <!-- Message displayed when a connection is insecure and we detect the user is entering a password -->
<string name="logins_insecure_connection_warning">Αυτή η σύνδεση δεν είναι ασφαλής. Οι λογαριασμοί που εισάγονται εδώ ενδέχεται να παραβιαστούν.</string> <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 --> <!-- Learn more link that will link to a page with more information displayed when a connection is insecure and we detect the user is entering a password -->

View File

@ -1402,7 +1402,7 @@
<string name="search_delete_search_engine_success_message">%s dihapus</string> <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 --> <!-- 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--> <!-- 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> <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 --> <!-- Text on the disabled button while in progress. Placeholder replaced with app name -->

View File

@ -36,12 +36,18 @@
<string name="tab_tray_add_new_collection_name">Naam</string> <string name="tab_tray_add_new_collection_name">Naam</string>
<!-- Label of button in save to collection dialog for selecting a current collection --> <!-- Label of button in save to collection dialog for selecting a current collection -->
<string name="tab_tray_select_collection">Collectie selecteren</string> <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 --> <!-- 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> <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 --> <!-- 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> <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 --> <!-- 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> <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 --> <!-- Content description on checkmark while tab is selected in multiselect mode in tab tray -->
<string name="tab_tray_multiselect_selected_content_description">Geselecteerd</string> <string name="tab_tray_multiselect_selected_content_description">Geselecteerd</string>

View File

@ -20,6 +20,7 @@
<string name="pref_key_privacy_link" translatable="false">pref_key_privacy_link</string> <string name="pref_key_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" 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_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_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_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> <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_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_debug_settings" translatable="false">pref_key_debug_settings</string>
<string name="pref_key_open_tabs_count" translatable="false">pref_key_open_tabs_count</string> <string name="pref_key_open_tabs_count" translatable="false">pref_key_open_tabs_count</string>

View File

@ -36,6 +36,8 @@
<string name="preferences_debug_settings_use_new_search_experience">Use New Search Experience</string> <string name="preferences_debug_settings_use_new_search_experience">Use New Search Experience</string>
<!-- Label for the wait until first paint preference --> <!-- 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> <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]. --> <!-- Content description (not visible, for screen readers etc.) used to announce [LinkTextView]. -->
<string name="link_text_view_type_announcement" translatable="false">link</string> <string name="link_text_view_type_announcement" translatable="false">link</string>

View File

@ -300,6 +300,8 @@
<string name="preferences_external_download_manager">External download manager</string> <string name="preferences_external_download_manager">External download manager</string>
<!-- Preference for add_ons --> <!-- Preference for add_ons -->
<string name="preferences_addons">Add-ons</string> <string name="preferences_addons">Add-ons</string>
<!-- Preference for notifications -->
<string name="preferences_notifications">Notifications</string>
<!-- Account Preferences --> <!-- Account Preferences -->
<!-- Preference for triggering sync --> <!-- Preference for triggering sync -->
@ -563,6 +565,13 @@
<!-- Text shown when no history exists --> <!-- Text shown when no history exists -->
<string name="history_empty_message">No history here</string> <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 --> <!-- Crashes -->
<!-- Title text displayed on the tab crash page. This first parameter is the name of the application (For example: Fenix) --> <!-- Title text displayed on the tab crash page. This first parameter is the name of the application (For example: Fenix) -->
<string name="tab_crash_title_2">Sorry. %1$s cant load that page.</string> <string name="tab_crash_title_2">Sorry. %1$s cant load that page.</string>
@ -1446,6 +1455,8 @@
<string name="synced_tabs_sign_in_message">View a list of tabs from your other devices.</string> <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 --> <!-- 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> <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 --> <!-- Top Sites -->
<!-- Title text displayed in the dialog when top sites limit is reached. --> <!-- Title text displayed in the dialog when top sites limit is reached. -->

View File

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

View File

@ -109,6 +109,11 @@
android:key="@string/pref_key_delete_browsing_data_on_quit_preference" android:key="@string/pref_key_delete_browsing_data_on_quit_preference"
android:title="@string/preferences_delete_browsing_data_on_quit" /> 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 <androidx.preference.Preference
android:icon="@drawable/ic_data_collection" android:icon="@drawable/ic_data_collection"
android:key="@string/pref_key_data_choices" android:key="@string/pref_key_data_choices"

View File

@ -14,4 +14,9 @@
android:key="@string/pref_key_wait_first_paint" android:key="@string/pref_key_wait_first_paint"
android:title="@string/preferences_debug_settings_wait_first_paint" android:title="@string/preferences_debug_settings_wait_first_paint"
app:iconSpaceReserved="false" /> 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> </PreferenceScreen>

View File

@ -0,0 +1,56 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.downloads
import androidx.recyclerview.widget.RecyclerView
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class DownloadAdapterTest {
private lateinit var interactor: DownloadInteractor
private lateinit var adapter: DownloadAdapter
@Before
fun setup() {
interactor = mockk()
adapter = DownloadAdapter(interactor)
every { interactor.select(any()) } just Runs
}
@Test
fun `getItemCount should return the number of tab collections`() {
val download = mockk<DownloadItem>()
assertEquals(0, adapter.itemCount)
adapter.updateDownloads(
downloads = listOf(download)
)
assertEquals(1, adapter.itemCount)
}
@Test
fun `updateData inserts item`() {
val download = mockk<DownloadItem> {
}
val observer = mockk<RecyclerView.AdapterDataObserver>(relaxed = true)
adapter.registerAdapterDataObserver(observer)
adapter.updateDownloads(
downloads = listOf(download)
)
verify { observer.onChanged() }
}
}

View File

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

View File

@ -0,0 +1,40 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.downloads
import io.mockk.every
import io.mockk.mockk
import io.mockk.verifyAll
import org.junit.Assert.assertTrue
import org.junit.Test
class DownloadInteractorTest {
private val downloadItem = DownloadItem(0, "title", "url", "5.6 mb", "png")
val controller: DownloadController = mockk(relaxed = true)
val interactor = DownloadInteractor(controller)
@Test
fun onOpen() {
interactor.open(downloadItem)
verifyAll {
controller.handleOpen(downloadItem)
}
}
@Test
fun onBackPressed() {
every {
controller.handleBackPressed()
} returns true
val backpressHandled = interactor.onBackPressed()
verifyAll {
controller.handleBackPressed()
}
assertTrue(backpressHandled)
}
}

View File

@ -0,0 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.sync
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.junit.Test
class ListenerDelegateTest {
@Test
fun `delegate invokes nullable listener`() {
val listener: SyncedTabsView.Listener? = mockk(relaxed = true)
val delegate = ListenerDelegate { listener }
delegate.onRefresh()
verify { listener?.onRefresh() }
delegate.onTabClicked(mockk())
verify { listener?.onTabClicked(any()) }
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,94 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.sync.ext
import io.mockk.every
import io.mockk.mockk
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.browser.storage.sync.Tab
import mozilla.components.browser.storage.sync.TabEntry
import mozilla.components.concept.sync.DeviceType
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mozilla.fenix.sync.SyncedTabsAdapter
class SyncedTabsAdapterKtTest {
private val noTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Charcoal"
every { deviceType } returns DeviceType.DESKTOP
},
tabs = emptyList()
)
private val oneTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Charcoal"
every { deviceType } returns DeviceType.DESKTOP
},
tabs = listOf(Tab(
history = listOf(TabEntry(
title = "Mozilla",
url = "https://mozilla.org",
iconUrl = null
)),
active = 0,
lastUsed = 0L
))
)
private val twoTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Emerald"
every { deviceType } returns DeviceType.MOBILE
},
tabs = listOf(
Tab(
history = listOf(TabEntry(
title = "Mozilla",
url = "https://mozilla.org",
iconUrl = null
)),
active = 0,
lastUsed = 0L
),
Tab(
history = listOf(
TabEntry(
title = "Firefox",
url = "https://firefox.com",
iconUrl = null
)
),
active = 0,
lastUsed = 0L
)
)
)
@Test
fun `verify ordering of adapter items`() {
val syncedDeviceList = listOf(oneTabDevice, twoTabDevice)
val adapterData = syncedDeviceList.toAdapterList()
assertEquals(5, adapterData.count())
assertTrue(adapterData[0] is SyncedTabsAdapter.AdapterItem.Device)
assertTrue(adapterData[1] is SyncedTabsAdapter.AdapterItem.Tab)
assertTrue(adapterData[2] is SyncedTabsAdapter.AdapterItem.Device)
assertTrue(adapterData[3] is SyncedTabsAdapter.AdapterItem.Tab)
assertTrue(adapterData[4] is SyncedTabsAdapter.AdapterItem.Tab)
}
@Test
fun `verify no tabs displayed`() {
val syncedDeviceList = listOf(noTabDevice)
val adapterData = syncedDeviceList.toAdapterList()
assertEquals(2, adapterData.count())
assertTrue(adapterData[0] is SyncedTabsAdapter.AdapterItem.Device)
assertTrue(adapterData[1] is SyncedTabsAdapter.AdapterItem.NoTabs)
}
}

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long