From d167bc7b673cb7753112d914d27ee219065981b4 Mon Sep 17 00:00:00 2001 From: Jonathan Almeida Date: Thu, 11 Mar 2021 18:49:05 +0400 Subject: [PATCH] Issue #14117: Add Synced Tabs as a page in the tabs tray --- .../fenix/tabstray/TabsTrayFragment.kt | 20 +++- .../fenix/tabstray/TrayPagerAdapter.kt | 16 ++- .../syncedtabs/SyncedTabsInteractor.kt | 30 +++++ .../syncedtabs/SyncedTabsTrayLayout.kt | 111 ++++++++++++++++++ .../viewholders/SyncedTabViewHolder.kt | 30 +++++ .../component_sync_tabs_tray_layout.xml | 29 +++++ .../main/res/layout/component_tabstray2.xml | 8 ++ 7 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsInteractor.kt create mode 100644 app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsTrayLayout.kt create mode 100644 app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabViewHolder.kt create mode 100644 app/src/main/res/layout/component_sync_tabs_tray_layout.xml diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt index 98da16122c..9b9fc8076a 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt @@ -15,12 +15,14 @@ import androidx.navigation.fragment.findNavController import com.google.android.material.bottomsheet.BottomSheetBehavior import kotlinx.android.synthetic.main.component_tabstray2.* import kotlinx.android.synthetic.main.component_tabstray2.view.* +import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor import org.mozilla.fenix.tabstray.browser.DefaultBrowserTrayInteractor import org.mozilla.fenix.tabstray.browser.RemoveTabUseCaseWrapper import org.mozilla.fenix.tabstray.browser.SelectTabUseCaseWrapper +import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsInteractor class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor { @@ -70,7 +72,13 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor { removeUseCases ) - setupPager(view.context, this, browserTrayInteractor) + val syncedTabsTrayInteractor = SyncedTabsInteractor( + requireComponents.analytics.metrics, + requireActivity() as HomeActivity, + this + ) + + setupPager(view.context, this, browserTrayInteractor, syncedTabsTrayInteractor) TabLayoutMediator( tabLayout = tab_layout, @@ -109,10 +117,16 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor { private fun setupPager( context: Context, trayInteractor: TabsTrayInteractor, - browserInteractor: BrowserTrayInteractor + browserInteractor: BrowserTrayInteractor, + syncedTabsTrayInteractor: SyncedTabsInteractor ) { tabsTray.apply { - adapter = TrayPagerAdapter(context, trayInteractor, browserInteractor) + adapter = TrayPagerAdapter( + context, + trayInteractor, + browserInteractor, + syncedTabsTrayInteractor + ) isUserInputEnabled = false } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt index ab82d94fb4..9d2ce04030 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt @@ -14,15 +14,20 @@ import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor import org.mozilla.fenix.tabstray.viewholders.AbstractTrayViewHolder import org.mozilla.fenix.tabstray.viewholders.NormalBrowserTabViewHolder import org.mozilla.fenix.tabstray.viewholders.PrivateBrowserTabViewHolder +import mozilla.components.feature.syncedtabs.view.SyncedTabsView +import org.mozilla.fenix.sync.SyncedTabsAdapter +import org.mozilla.fenix.tabstray.viewholders.SyncedTabViewHolder class TrayPagerAdapter( val context: Context, val interactor: TabsTrayInteractor, - val browserInteractor: BrowserTrayInteractor + val browserInteractor: BrowserTrayInteractor, + val syncedTabsInteractor: SyncedTabsView.Listener ) : RecyclerView.Adapter() { private val normalAdapter by lazy { BrowserTabsAdapter(context, browserInteractor) } private val privateAdapter by lazy { BrowserTabsAdapter(context, browserInteractor) } + private val syncedTabsAdapter by lazy { SyncedTabsAdapter(syncedTabsInteractor) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractTrayViewHolder { val itemView = LayoutInflater.from(parent.context).inflate(viewType, parent, false) @@ -36,6 +41,10 @@ class TrayPagerAdapter( itemView, interactor ) + SyncedTabViewHolder.LAYOUT_ID -> SyncedTabViewHolder( + itemView, + syncedTabsInteractor + ) else -> throw IllegalStateException("Unknown viewType.") } } @@ -44,6 +53,7 @@ class TrayPagerAdapter( val adapter = when (position) { POSITION_NORMAL_TABS -> normalAdapter POSITION_PRIVATE_TABS -> privateAdapter + POSITION_SYNCED_TABS -> syncedTabsAdapter else -> throw IllegalStateException("View type does not exist.") } @@ -54,6 +64,7 @@ class TrayPagerAdapter( return when (position) { POSITION_NORMAL_TABS -> NormalBrowserTabViewHolder.LAYOUT_ID POSITION_PRIVATE_TABS -> PrivateBrowserTabViewHolder.LAYOUT_ID + POSITION_SYNCED_TABS -> SyncedTabViewHolder.LAYOUT_ID else -> throw IllegalStateException("Unknown position.") } } @@ -61,9 +72,10 @@ class TrayPagerAdapter( override fun getItemCount(): Int = TRAY_TABS_COUNT companion object { - const val TRAY_TABS_COUNT = 2 + const val TRAY_TABS_COUNT = 3 const val POSITION_NORMAL_TABS = 0 const val POSITION_PRIVATE_TABS = 1 + const val POSITION_SYNCED_TABS = 2 } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsInteractor.kt new file mode 100644 index 0000000000..c098ba0972 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsInteractor.kt @@ -0,0 +1,30 @@ +/* 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.tabstray.syncedtabs + +import mozilla.components.browser.storage.sync.Tab +import mozilla.components.feature.syncedtabs.view.SyncedTabsView +import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.tabstray.TabsTrayInteractor + +internal class SyncedTabsInteractor( + private val metrics: MetricController, + private val activity: HomeActivity, + private val trayInteractor: TabsTrayInteractor +) : SyncedTabsView.Listener { + override fun onRefresh() = Unit + override fun onTabClicked(tab: Tab) { + metrics.track(Event.SyncedTabOpened) + activity.openToBrowserAndLoad( + searchTermOrURL = tab.active().url, + newTab = true, + from = BrowserDirection.FromTabTray + ) + trayInteractor.navigateToBrowser() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsTrayLayout.kt b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsTrayLayout.kt new file mode 100644 index 0000000000..93bc608310 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsTrayLayout.kt @@ -0,0 +1,111 @@ +/* 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.tabstray.syncedtabs + +import android.content.Context +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.findFragment +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import kotlinx.android.synthetic.main.component_sync_tabs_tray_layout.view.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import mozilla.components.browser.storage.sync.SyncedDeviceTabs +import mozilla.components.feature.syncedtabs.SyncedTabsFeature +import mozilla.components.feature.syncedtabs.view.SyncedTabsView +import mozilla.components.support.base.observer.Observable +import mozilla.components.support.base.observer.ObserverRegistry +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.sync.SyncedTabsAdapter +import org.mozilla.fenix.sync.ext.toAdapterItem +import org.mozilla.fenix.sync.ext.toStringRes +import org.mozilla.fenix.tabstray.TabsTrayFragment +import org.mozilla.fenix.tabstray.TrayItem +import org.mozilla.fenix.utils.view.LifecycleViewProvider + +class SyncedTabsTrayLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr), SyncedTabsView, TrayItem, + Observable by ObserverRegistry() { + + private val lifecycleProvider = LifecycleViewProvider(this) + private val coroutineScope = CoroutineScope(Dispatchers.Main) + private val syncedTabsFeature by lazy { + SyncedTabsFeature( + context = context, + storage = context.components.backgroundServices.syncedTabsStorage, + accountManager = context.components.backgroundServices.accountManager, + view = this, + lifecycleOwner = lifecycleProvider, + onTabClicked = { + // We can ignore this callback here because we're not connecting the adapter + // back to the feature. This works fine in other features, but passing the listener + // to other components in this case is annoying. + } + ) + } + + override var listener: SyncedTabsView.Listener? = null + + override fun displaySyncedTabs(syncedTabs: List) { + coroutineScope.launch { + (synced_tabs_list.adapter as SyncedTabsAdapter).updateData(syncedTabs) + } + } + + override fun onError(error: SyncedTabsView.ErrorType) { + coroutineScope.launch { + // We may still be displaying a "loading" spinner, hide it. + stopLoading() + + val navController: NavController? = try { + findFragment().findNavController() + } catch (exception: IllegalStateException) { + null + } + + val descriptionResId = error.toStringRes() + val errorItem = error.toAdapterItem(descriptionResId, navController) + + val errorList: List = listOf(errorItem) + (synced_tabs_list.adapter as SyncedTabsAdapter).submitList(errorList) + } + } + + override fun startLoading() { + // TODO implement with https://github.com/mozilla-mobile/fenix/issues/18515 + // start animating FAB + // interactor.syncingTabs(loading = true) + } + + override fun stopLoading() { + // TODO implement with https://github.com/mozilla-mobile/fenix/issues/18515 + // stop animating FAB + // interactor.syncingTabs(loading = false) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + + syncedTabsFeature.start() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + + syncedTabsFeature.stop() + coroutineScope.cancel() + } + + fun onRefresh() { + // TODO implement with https://github.com/mozilla-mobile/fenix/issues/18515 + // syncedTabsFeature.interactor.onRefresh() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabViewHolder.kt new file mode 100644 index 0000000000..9aec84ff9b --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabViewHolder.kt @@ -0,0 +1,30 @@ +/* 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.tabstray.viewholders + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.component_sync_tabs_tray_layout.* +import mozilla.components.feature.syncedtabs.view.SyncedTabsView +import org.mozilla.fenix.R + +class SyncedTabViewHolder( + containerView: View, + private val listener: SyncedTabsView.Listener +) : AbstractTrayViewHolder(containerView) { + + override fun bind( + adapter: RecyclerView.Adapter, + layoutManager: RecyclerView.LayoutManager + ) { + synced_tabs_list.layoutManager = layoutManager + synced_tabs_list.adapter = adapter + synced_tabs_tray_layout.listener = listener + } + + companion object { + const val LAYOUT_ID = R.layout.component_sync_tabs_tray_layout + } +} diff --git a/app/src/main/res/layout/component_sync_tabs_tray_layout.xml b/app/src/main/res/layout/component_sync_tabs_tray_layout.xml new file mode 100644 index 0000000000..451b9a780e --- /dev/null +++ b/app/src/main/res/layout/component_sync_tabs_tray_layout.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/layout/component_tabstray2.xml b/app/src/main/res/layout/component_tabstray2.xml index 6bcbe4e4f8..7d460b616f 100644 --- a/app/src/main/res/layout/component_tabstray2.xml +++ b/app/src/main/res/layout/component_tabstray2.xml @@ -119,6 +119,14 @@ android:contentDescription="@string/tabs_header_private_tabs_title" android:icon="@drawable/ic_private_browsing" /> + +