diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt index 93b5408af..1a29fb83a 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt @@ -33,6 +33,12 @@ data class Tab( val icon: Bitmap? = null ) +data class TopSiteItem( + override val id: Long, + override val title: String, + override val url: String +) : TopSite + fun List.toSessionBundle(sessionManager: SessionManager): List { return this.mapNotNull { sessionManager.findSessionById(it.sessionId) } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt index 574427056..bbfa546ba 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt @@ -17,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.tab_list_row.* import mozilla.components.feature.media.state.MediaState import mozilla.components.feature.tab.collections.TabCollection +import mozilla.components.feature.top.sites.TopSite import org.mozilla.fenix.home.OnboardingState import org.mozilla.fenix.home.Tab import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder @@ -27,6 +28,7 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.SaveTabGroupViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.TabHeaderViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.TabViewHolder +import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSiteViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingAutomaticSignInViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingFinishViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingHeaderViewHolder @@ -65,6 +67,8 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) { } } + data class TopSiteList(val topSites: List) : AdapterItem(TopSiteViewHolder.LAYOUT_ID) + object SaveTabGroup : AdapterItem(SaveTabGroupViewHolder.LAYOUT_ID) object PrivateBrowsingDescription : AdapterItem(PrivateBrowsingDescriptionViewHolder.LAYOUT_ID) @@ -148,6 +152,7 @@ class SessionControlAdapter( return when (viewType) { TabHeaderViewHolder.LAYOUT_ID -> TabHeaderViewHolder(view, interactor) TabViewHolder.LAYOUT_ID -> TabViewHolder(view, interactor) + TopSiteViewHolder.LAYOUT_ID -> TopSiteViewHolder(view, interactor) SaveTabGroupViewHolder.LAYOUT_ID -> SaveTabGroupViewHolder(view, interactor) PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(view, interactor) NoContentMessageViewHolder.LAYOUT_ID -> NoContentMessageViewHolder(view) @@ -169,6 +174,7 @@ class SessionControlAdapter( override fun getItemViewType(position: Int) = getItem(position).viewType + @SuppressWarnings("ComplexMethod") override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val item = getItem(position) when (holder) { @@ -179,6 +185,9 @@ class SessionControlAdapter( is TabViewHolder -> { holder.bindSession((item as AdapterItem.TabItem).tab) } + is TopSiteViewHolder -> { + holder.bind((item as AdapterItem.TopSiteList).topSites) + } is NoContentMessageViewHolder -> { val (icon, header, description) = item as AdapterItem.NoContentMessage holder.bind(icon, header, description) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt index 3137b1cc4..1cec01474 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt @@ -112,6 +112,11 @@ interface SessionControlController { */ fun handleSelectTab(tabView: View, sessionId: String) + /** + * @see [TopSiteInteractor.onSelectTopSite] + */ + fun handleSelectTopSite(url: String) + /** * @see [TabSessionInteractor.onShareTabs] */ @@ -298,6 +303,14 @@ class DefaultSessionControlController( navController.nav(R.id.homeFragment, directions, extras) } + override fun handleSelectTopSite(url: String) { + activity.components.useCases.tabsUseCases.addTab.invoke(url, true, true) + navController.nav( + R.id.homeFragment, + HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null) + ) + } + override fun handleShareTabs() { invokePendingDeleteJobs() val shareData = sessionManager diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt index 3995864af..2fa4bf0d7 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt @@ -149,15 +149,27 @@ interface TabSessionInteractor { fun onShareTabs() } +/** + * Interface for top site related actions in the [SessionControlInteractor]. + */ +interface TopSiteInteractor { + /** + * Selects the given top site. Called when a user clicks on a top site. + * + * @param url The URL of the top site. + */ + fun onSelectTopSite(url: String) +} + /** * Interactor for the Home screen. - * Provides implementations for the CollectionInteractor, OnboardingInteractor and - * TabSessionInteractor. + * Provides implementations for the CollectionInteractor, OnboardingInteractor, + * TabSessionInteractor and TopSiteInteractor. */ @SuppressWarnings("TooManyFunctions") class SessionControlInteractor( private val controller: SessionControlController -) : CollectionInteractor, OnboardingInteractor, TabSessionInteractor { +) : CollectionInteractor, OnboardingInteractor, TabSessionInteractor, TopSiteInteractor { override fun onCloseTab(sessionId: String) { controller.handleCloseTab(sessionId) } @@ -214,6 +226,10 @@ class SessionControlInteractor( controller.handleSelectTab(tabView, sessionId) } + override fun onSelectTopSite(url: String) { + controller.handleSelectTopSite(url) + } + override fun onShareTabs() { controller.handleShareTabs() } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt index d4e6e44c5..cdfc6510f 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt @@ -15,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView import kotlinx.android.extensions.LayoutContainer import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.feature.tab.collections.TabCollection +import mozilla.components.feature.top.sites.TopSite import mozilla.components.lib.state.ext.consumeFrom import org.mozilla.fenix.R import org.mozilla.fenix.home.HomeFragmentState @@ -37,10 +38,16 @@ val noCollectionMessage = AdapterItem.NoContentMessage( private fun normalModeAdapterItems( tabs: List, + topSites: List, collections: List, expandedCollections: Set ): List { val items = mutableListOf() + + if (topSites.isNotEmpty()) { + items.add(AdapterItem.TopSiteList(topSites)) + } + items.add(AdapterItem.TabHeader(false, tabs.isNotEmpty())) if (tabs.isNotEmpty()) { @@ -52,7 +59,6 @@ private fun normalModeAdapterItems( items.add(AdapterItem.CollectionHeader) if (collections.isNotEmpty()) { - // If the collection is expanded, we want to add all of its tabs beneath it in the adapter collections.map { AdapterItem.CollectionItem(it, expandedCollections.contains(it.id), tabs.isNotEmpty()) @@ -116,7 +122,7 @@ private fun onboardingAdapterItems(onboardingState: OnboardingState): List = when (mode) { - is Mode.Normal -> normalModeAdapterItems(tabs, collections, expandedCollections) + is Mode.Normal -> normalModeAdapterItems(tabs, topSites, collections, expandedCollections) is Mode.Private -> privateModeAdapterItems(tabs) is Mode.Onboarding -> onboardingAdapterItems(mode.state) } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TopSiteViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TopSiteViewHolder.kt new file mode 100644 index 000000000..dd44ad623 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TopSiteViewHolder.kt @@ -0,0 +1,39 @@ +/* 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.home.sessioncontrol.viewholders + +import android.view.View +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.component_top_sites.view.* +import mozilla.components.feature.top.sites.TopSite +import org.mozilla.fenix.R +import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor +import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.TopSitesAdapter + +class TopSiteViewHolder( + view: View, + interactor: TopSiteInteractor, + override val containerView: View? = view +) : RecyclerView.ViewHolder(view), LayoutContainer { + private val topSitesAdapter = TopSitesAdapter(interactor) + + init { + view.top_sites_list.apply { + adapter = topSitesAdapter + layoutManager = GridLayoutManager(view.context, NUM_COLUMNS) + } + } + + fun bind(topSites: List) { + topSitesAdapter.submitList(topSites) + } + + companion object { + const val LAYOUT_ID = R.layout.component_top_sites + const val NUM_COLUMNS = 5 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSiteItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSiteItemViewHolder.kt new file mode 100644 index 000000000..a668af173 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSiteItemViewHolder.kt @@ -0,0 +1,37 @@ +/* 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.home.sessioncontrol.viewholders.topsites + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.top_site_item.view.* +import mozilla.components.feature.top.sites.TopSite +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.loadIntoView +import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor + +class TopSiteItemViewHolder( + private val view: View, + private val interactor: TopSiteInteractor +) : RecyclerView.ViewHolder(view) { + private lateinit var topSite: TopSite + + init { + view.top_site_item.setOnClickListener { + interactor.onSelectTopSite(topSite.url) + } + } + + fun bind(topSite: TopSite) { + this.topSite = topSite + view.top_site_title.text = topSite.title + view.context.components.core.icons.loadIntoView(view.favicon_image, topSite.url) + } + + companion object { + const val LAYOUT_ID = R.layout.top_site_item + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesAdapter.kt new file mode 100644 index 000000000..7134931a8 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesAdapter.kt @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.home.sessioncontrol.viewholders.topsites + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import mozilla.components.feature.top.sites.TopSite +import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor + +class TopSitesAdapter( + private val interactor: TopSiteInteractor +) : ListAdapter(DiffCallback) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopSiteItemViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(TopSiteItemViewHolder.LAYOUT_ID, parent, false) + return TopSiteItemViewHolder(view, interactor) + } + + override fun onBindViewHolder(holder: TopSiteItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + private object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: TopSite, newItem: TopSite) = + oldItem.id == newItem.id || oldItem.title == newItem.title || oldItem.url == newItem.url + + override fun areContentsTheSame(oldItem: TopSite, newItem: TopSite) = + oldItem.id == newItem.id || oldItem.title == newItem.title || oldItem.url == newItem.url + } +} diff --git a/app/src/main/res/layout/component_top_sites.xml b/app/src/main/res/layout/component_top_sites.xml new file mode 100644 index 000000000..937cfa598 --- /dev/null +++ b/app/src/main/res/layout/component_top_sites.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/layout/top_site_item.xml b/app/src/main/res/layout/top_site_item.xml new file mode 100644 index 000000000..ddf593182 --- /dev/null +++ b/app/src/main/res/layout/top_site_item.xml @@ -0,0 +1,40 @@ + + + + + + + + +