diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 1cc53784d..f49559573 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -205,8 +205,10 @@ class HomeFragment : Fragment(), CoroutineScope, AccountObserver { BrowsingModeManager.Mode.Private -> BrowsingModeManager.Mode.Normal } - val mode = if (newMode == BrowsingModeManager.Mode.Private) Mode.Private else Mode.Normal - getManagedEmitter().onNext(SessionControlChange.ModeChange(mode)) + if (onboarding.userHasBeenOnboarded()) { + val mode = if (newMode == BrowsingModeManager.Mode.Private) Mode.Private else Mode.Normal + getManagedEmitter().onNext(SessionControlChange.ModeChange(mode)) + } browsingModeManager.mode = newMode } 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 893a7a290..b7891dc71 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 @@ -7,18 +7,22 @@ package org.mozilla.fenix.home.sessioncontrol import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup +import androidx.annotation.DrawableRes +import androidx.annotation.LayoutRes +import androidx.annotation.StringRes +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import io.reactivex.Observer import kotlinx.coroutines.Job -import org.mozilla.fenix.home.sessioncontrol.viewholders.SaveTabGroupViewHolder -import org.mozilla.fenix.home.sessioncontrol.viewholders.NoTabMessageViewHolder -import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder -import org.mozilla.fenix.home.sessioncontrol.viewholders.TabHeaderViewHolder -import org.mozilla.fenix.home.sessioncontrol.viewholders.TabViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder -import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionMessageViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder +import org.mozilla.fenix.home.sessioncontrol.viewholders.NoContentMessageViewHolder +import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder +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.onboarding.OnboardingFinishViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingFirefoxAccountViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingHeaderViewHolder @@ -28,69 +32,69 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingSe import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingThemePickerViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingTrackingProtectionViewHolder import mozilla.components.feature.tab.collections.Tab as ComponentTab -import java.lang.IllegalStateException -sealed class AdapterItem { - data class TabHeader(val isPrivate: Boolean, val hasTabs: Boolean) : AdapterItem() - object NoTabMessage : AdapterItem() - data class TabItem(val tab: Tab) : AdapterItem() - object SaveTabGroup : AdapterItem() +sealed class AdapterItem(@LayoutRes val viewType: Int) { + data class TabHeader(val isPrivate: Boolean, val hasTabs: Boolean) : AdapterItem(TabHeaderViewHolder.LAYOUT_ID) + data class TabItem(val tab: Tab) : AdapterItem(TabViewHolder.LAYOUT_ID) { + override fun sameAs(other: AdapterItem) = other is TabItem && tab.sessionId == other.tab.sessionId + } + object SaveTabGroup : AdapterItem(SaveTabGroupViewHolder.LAYOUT_ID) - object PrivateBrowsingDescription : AdapterItem() + object PrivateBrowsingDescription : AdapterItem(PrivateBrowsingDescriptionViewHolder.LAYOUT_ID) + data class NoContentMessage( + @DrawableRes val icon: Int, + @StringRes val header: Int, + @StringRes val description: Int + ) : AdapterItem(NoContentMessageViewHolder.LAYOUT_ID) - object CollectionHeader : AdapterItem() - object NoCollectionMessage : AdapterItem() - data class CollectionItem(val collection: TabCollection) : AdapterItem() + object CollectionHeader : AdapterItem(CollectionHeaderViewHolder.LAYOUT_ID) + data class CollectionItem( + val collection: TabCollection, + val expanded: Boolean + ) : AdapterItem(CollectionViewHolder.LAYOUT_ID) { + override fun sameAs(other: AdapterItem) = other is CollectionItem && collection.id == other.collection.id + } data class TabInCollectionItem( val collection: TabCollection, val tab: ComponentTab, val isLastTab: Boolean - ) : AdapterItem() + ) : AdapterItem(TabInCollectionViewHolder.LAYOUT_ID) { + override fun sameAs(other: AdapterItem) = other is TabInCollectionItem && tab.id == other.tab.id + } - object OnboardingHeader : AdapterItem() - data class OnboardingSectionHeader(val labelBuilder: (Context) -> String) : AdapterItem() - data class OnboardingFirefoxAccount(val state: OnboardingState) : AdapterItem() - object OnboardingThemePicker : AdapterItem() - object OnboardingTrackingProtection : AdapterItem() - object OnboardingPrivateBrowsing : AdapterItem() - object OnboardingPrivacyNotice : AdapterItem() - object OnboardingFinish : AdapterItem() + object OnboardingHeader : AdapterItem(OnboardingHeaderViewHolder.LAYOUT_ID) + data class OnboardingSectionHeader( + val labelBuilder: (Context) -> String + ) : AdapterItem(OnboardingSectionHeaderViewHolder.LAYOUT_ID) { + override fun sameAs(other: AdapterItem) = other is OnboardingSectionHeader && labelBuilder == other.labelBuilder + } + data class OnboardingFirefoxAccount( + val state: OnboardingState + ) : AdapterItem(OnboardingFirefoxAccountViewHolder.LAYOUT_ID) + object OnboardingThemePicker : AdapterItem(OnboardingThemePickerViewHolder.LAYOUT_ID) + object OnboardingTrackingProtection : AdapterItem(OnboardingTrackingProtectionViewHolder.LAYOUT_ID) + object OnboardingPrivateBrowsing : AdapterItem(OnboardingPrivateBrowsingViewHolder.LAYOUT_ID) + object OnboardingPrivacyNotice : AdapterItem(OnboardingPrivacyNoticeViewHolder.LAYOUT_ID) + object OnboardingFinish : AdapterItem(OnboardingFinishViewHolder.LAYOUT_ID) - val viewType: Int - get() = when (this) { - is TabHeader -> TabHeaderViewHolder.LAYOUT_ID - NoTabMessage -> NoTabMessageViewHolder.LAYOUT_ID - is TabItem -> TabViewHolder.LAYOUT_ID - SaveTabGroup -> SaveTabGroupViewHolder.LAYOUT_ID - PrivateBrowsingDescription -> PrivateBrowsingDescriptionViewHolder.LAYOUT_ID - CollectionHeader -> CollectionHeaderViewHolder.LAYOUT_ID - NoCollectionMessage -> NoCollectionMessageViewHolder.LAYOUT_ID - is CollectionItem -> CollectionViewHolder.LAYOUT_ID - is TabInCollectionItem -> TabInCollectionViewHolder.LAYOUT_ID - OnboardingHeader -> OnboardingHeaderViewHolder.LAYOUT_ID - is OnboardingSectionHeader -> OnboardingSectionHeaderViewHolder.LAYOUT_ID - is OnboardingFirefoxAccount -> OnboardingFirefoxAccountViewHolder.LAYOUT_ID - OnboardingThemePicker -> OnboardingThemePickerViewHolder.LAYOUT_ID - OnboardingTrackingProtection -> OnboardingTrackingProtectionViewHolder.LAYOUT_ID - OnboardingPrivateBrowsing -> OnboardingPrivateBrowsingViewHolder.LAYOUT_ID - OnboardingPrivacyNotice -> OnboardingPrivacyNoticeViewHolder.LAYOUT_ID - OnboardingFinish -> OnboardingFinishViewHolder.LAYOUT_ID - } + /** + * True if this item represents the same value as other. Used by [AdapterItemDiffCallback]. + */ + open fun sameAs(other: AdapterItem) = this::class == other::class +} + +class AdapterItemDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = oldItem.sameAs(newItem) + + @Suppress("DiffUtilEquals") + override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = oldItem == newItem } class SessionControlAdapter( private val actionEmitter: Observer -) : RecyclerView.Adapter() { +) : ListAdapter(AdapterItemDiffCallback()) { - private var items: List = listOf() private lateinit var job: Job - private lateinit var expandedCollections: Set - - fun reloadData(items: List, expandedCollections: Set) { - this.items = items - this.expandedCollections = expandedCollections - notifyDataSetChanged() - } // This method triggers the ComplexMethod lint error when in fact it's quite simple. @SuppressWarnings("ComplexMethod") @@ -98,12 +102,11 @@ class SessionControlAdapter( val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) return when (viewType) { TabHeaderViewHolder.LAYOUT_ID -> TabHeaderViewHolder(view, actionEmitter) - NoTabMessageViewHolder.LAYOUT_ID -> NoTabMessageViewHolder(view) TabViewHolder.LAYOUT_ID -> TabViewHolder(view, actionEmitter, job) SaveTabGroupViewHolder.LAYOUT_ID -> SaveTabGroupViewHolder(view, actionEmitter) PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(view, actionEmitter) + NoContentMessageViewHolder.LAYOUT_ID -> NoContentMessageViewHolder(view) CollectionHeaderViewHolder.LAYOUT_ID -> CollectionHeaderViewHolder(view) - NoCollectionMessageViewHolder.LAYOUT_ID -> NoCollectionMessageViewHolder(view) CollectionViewHolder.LAYOUT_ID -> CollectionViewHolder(view, actionEmitter, job) TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder(view, actionEmitter, job) OnboardingHeaderViewHolder.LAYOUT_ID -> OnboardingHeaderViewHolder(view) @@ -128,32 +131,35 @@ class SessionControlAdapter( job.cancel() } - override fun getItemViewType(position: Int) = items[position].viewType - - override fun getItemCount(): Int = items.size + override fun getItemViewType(position: Int) = getItem(position).viewType override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = getItem(position) when (holder) { is TabHeaderViewHolder -> { - val tabHeader = items[position] as AdapterItem.TabHeader + val tabHeader = item as AdapterItem.TabHeader holder.bind(tabHeader.isPrivate, tabHeader.hasTabs) } is TabViewHolder -> holder.bindSession( - (items[position] as AdapterItem.TabItem).tab + (item as AdapterItem.TabItem).tab ) + is NoContentMessageViewHolder -> { + val (icon, header, description) = item as AdapterItem.NoContentMessage + holder.bind(icon, header, description) + } is CollectionViewHolder -> { - val collection = (items[position] as AdapterItem.CollectionItem).collection - holder.bindSession(collection, expandedCollections.contains(collection.id)) + val (collection, expanded) = item as AdapterItem.CollectionItem + holder.bindSession(collection, expanded) } is TabInCollectionViewHolder -> { - val item = items[position] as AdapterItem.TabInCollectionItem - holder.bindSession(item.collection, item.tab, item.isLastTab) + val (collection, tab, isLastTab) = item as AdapterItem.TabInCollectionItem + holder.bindSession(collection, tab, isLastTab) } is OnboardingSectionHeaderViewHolder -> holder.bind( - (items[position] as AdapterItem.OnboardingSectionHeader).labelBuilder + (item as AdapterItem.OnboardingSectionHeader).labelBuilder ) is OnboardingFirefoxAccountViewHolder -> holder.bind( - (items[position] as AdapterItem.OnboardingFirefoxAccount).state == OnboardingState.AutoSignedIn + (item as AdapterItem.OnboardingFirefoxAccount).state == OnboardingState.AutoSignedIn ) } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlUIView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlUIView.kt index 83e05f03c..376fd4e71 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlUIView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlUIView.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.home.sessioncontrol import android.view.LayoutInflater import android.view.ViewGroup +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.reactivex.Observable @@ -13,7 +14,18 @@ import io.reactivex.Observer import io.reactivex.functions.Consumer import org.mozilla.fenix.R import org.mozilla.fenix.mvi.UIView -import androidx.recyclerview.widget.ItemTouchHelper + +val noTabMessage = AdapterItem.NoContentMessage( + R.drawable.ic_tabs, + R.string.no_open_tabs_header, + R.string.no_open_tabs_description +) + +val noCollectionMessage = AdapterItem.NoContentMessage( + R.drawable.ic_tab_collection, + R.string.no_collections_header, + R.string.no_collections_description +) private fun normalModeAdapterItems( tabs: List, @@ -27,21 +39,23 @@ private fun normalModeAdapterItems( items.addAll(tabs.reversed().map(AdapterItem::TabItem)) items.add(AdapterItem.SaveTabGroup) } else { - items.add(AdapterItem.NoTabMessage) + items.add(noTabMessage) } 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).forEach { + collections.map { + AdapterItem.CollectionItem(it, expandedCollections.contains(it.id)) + }.forEach { items.add(it) - if (it.collection.isExpanded(expandedCollections)) { + if (it.expanded) { items.addAll(collectionTabItems(it.collection)) } } } else { - items.add(AdapterItem.NoCollectionMessage) + items.add(noCollectionMessage) } return items @@ -104,10 +118,6 @@ private fun collectionTabItems(collection: TabCollection) = collection.tabs.mapI AdapterItem.TabInCollectionItem(collection, tab, index == collection.tabs.lastIndex) } -private fun TabCollection.isExpanded(expandedCollections: Set): Boolean { - return expandedCollections.contains(this.id) -} - class SessionControlUIView( container: ViewGroup, actionEmitter: Observer, @@ -129,6 +139,7 @@ class SessionControlUIView( view.apply { adapter = sessionControlAdapter layoutManager = LinearLayoutManager(container.context) + itemAnimator = null // TODO #2785: Remove this line val itemTouchHelper = ItemTouchHelper( SwipeToDeleteCallback( @@ -140,7 +151,7 @@ class SessionControlUIView( } override fun updateView() = Consumer { - sessionControlAdapter.reloadData(it.toAdapterList(), it.expandedCollections) + sessionControlAdapter.submitList(it.toAdapterList()) actionEmitter.onNext(SessionControlAction.ReloadData) } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CollectionViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CollectionViewHolder.kt index 8f02ecd5f..5ea2f4386 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CollectionViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CollectionViewHolder.kt @@ -84,11 +84,7 @@ class CollectionViewHolder( private fun updateCollectionUI() { view.collection_title.text = collection.title - var hostNameList = listOf() - - collection.tabs.forEach { - hostNameList += it.url.urlToTrimmedHost().capitalize() - } + val hostNameList = collection.tabs.map { it.url.urlToTrimmedHost().capitalize() } var tabsDisplayed = 0 val tabTitlesList = hostNameList.joinToString(", ") { diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoCollectionMessageViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoCollectionMessageViewHolder.kt deleted file mode 100644 index cffc4df77..000000000 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoCollectionMessageViewHolder.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* 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.RecyclerView -import org.mozilla.fenix.R - -class NoCollectionMessageViewHolder( - view: View -) : RecyclerView.ViewHolder(view) { - companion object { - const val LAYOUT_ID = R.layout.no_collection_message - } -} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoContentMessageViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoContentMessageViewHolder.kt new file mode 100644 index 000000000..d176ab77f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoContentMessageViewHolder.kt @@ -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.home.sessioncontrol.viewholders + +import android.view.View +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.no_content_message.view.* +import org.mozilla.fenix.R + +class NoContentMessageViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { + + fun bind( + @DrawableRes icon: Int, + @StringRes header: Int, + @StringRes description: Int + ) { + with(view.context) { + view.no_content_header.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, icon, 0) + view.no_content_header.text = getString(header) + view.no_content_description.text = getString(description) + } + } + + companion object { + const val LAYOUT_ID = R.layout.no_content_message + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoTabMessageViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoTabMessageViewHolder.kt deleted file mode 100644 index e96f214e1..000000000 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoTabMessageViewHolder.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* 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.RecyclerView -import org.mozilla.fenix.R - -class NoTabMessageViewHolder( - view: View -) : RecyclerView.ViewHolder(view) { - companion object { - const val LAYOUT_ID = R.layout.no_tab_message - } -} diff --git a/app/src/main/res/layout/no_collection_message.xml b/app/src/main/res/layout/no_content_message.xml similarity index 77% rename from app/src/main/res/layout/no_collection_message.xml rename to app/src/main/res/layout/no_content_message.xml index 0a5c7c9c0..648b67023 100644 --- a/app/src/main/res/layout/no_collection_message.xml +++ b/app/src/main/res/layout/no_content_message.xml @@ -4,7 +4,7 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + \ No newline at end of file diff --git a/app/src/main/res/layout/no_tab_message.xml b/app/src/main/res/layout/no_tab_message.xml deleted file mode 100644 index cf65d4589..000000000 --- a/app/src/main/res/layout/no_tab_message.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/onboarding_section_header.xml b/app/src/main/res/layout/onboarding_section_header.xml index 0d224bd40..78488feb3 100644 --- a/app/src/main/res/layout/onboarding_section_header.xml +++ b/app/src/main/res/layout/onboarding_section_header.xml @@ -4,13 +4,13 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + android:id="@+id/section_header_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAppearance="@style/HeaderTextStyle" tools:text="@tools:sample/lorem"/> \ No newline at end of file