1
0
Fork 0

For #2205 & #1578: Integrates tab collection storage (#2478)

* For #2205: Adds TabCollectionStorage

* For #1578: Adds delete to TabCollection
master
Sawyer Blatz 2019-05-16 14:02:24 -07:00 committed by GitHub
parent 892a4b7bf4
commit 72d29c2a43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 228 additions and 98 deletions

View File

@ -309,6 +309,7 @@ dependencies {
implementation Deps.mozilla_feature_session_bundling implementation Deps.mozilla_feature_session_bundling
implementation Deps.mozilla_feature_site_permissions implementation Deps.mozilla_feature_site_permissions
implementation Deps.mozilla_feature_readerview implementation Deps.mozilla_feature_readerview
implementation Deps.mozilla_feature_tab_collections
implementation Deps.mozilla_service_firefox_accounts implementation Deps.mozilla_service_firefox_accounts
implementation Deps.mozilla_service_fretboard implementation Deps.mozilla_service_fretboard

View File

@ -687,7 +687,7 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope {
launch { launch {
val host = session.url.toUri()?.host val host = session.url.toUri()?.host
val sitePermissions: SitePermissions? = host?.let { val sitePermissions: SitePermissions? = host?.let {
val storage = requireContext().components.storage val storage = requireContext().components.core.permissionStorage
storage.findSitePermissionsBy(it) storage.findSitePermissionsBy(it)
} }

View File

@ -14,20 +14,26 @@ import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_create_collection.view.* import kotlinx.android.synthetic.main.fragment_create_collection.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.mozilla.fenix.FenixViewModelProvider import org.mozilla.fenix.FenixViewModelProvider
import mozilla.components.browser.session.Session
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.ext.getRootView import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.home.sessioncontrol.TabCollection import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.mvi.getManagedEmitter import org.mozilla.fenix.mvi.getManagedEmitter
import java.util.Random import kotlin.coroutines.CoroutineContext
class CreateCollectionFragment : DialogFragment() { class CreateCollectionFragment : DialogFragment(), CoroutineScope {
// Temporary callback. In the future we will just directly add the collection to the core session manager.
var onCollectionSaved: ((TabCollection) -> Unit)? = null
private lateinit var collectionCreationComponent: CollectionCreationComponent private lateinit var collectionCreationComponent: CollectionCreationComponent
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -40,6 +46,7 @@ class CreateCollectionFragment : DialogFragment() {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
job = Job()
val view = inflater.inflate(R.layout.fragment_create_collection, container, false) val view = inflater.inflate(R.layout.fragment_create_collection, container, false)
val viewModel = activity?.run { val viewModel = activity?.run {
@ -76,6 +83,12 @@ class CreateCollectionFragment : DialogFragment() {
subscribeToActions() subscribeToActions()
} }
override fun onDestroyView() {
super.onDestroyView()
job.cancel()
}
@Suppress("ComplexMethod")
private fun subscribeToActions() { private fun subscribeToActions() {
getAutoDisposeObservable<CollectionCreationAction>().subscribe { getAutoDisposeObservable<CollectionCreationAction>().subscribe {
when (it) { when (it) {
@ -97,8 +110,17 @@ class CreateCollectionFragment : DialogFragment() {
is CollectionCreationAction.SaveCollectionName -> { is CollectionCreationAction.SaveCollectionName -> {
showSavedSnackbar(it.tabs.size) showSavedSnackbar(it.tabs.size)
dismiss() dismiss()
val newCollection = TabCollection(Random().nextInt(), it.name, it.tabs.toMutableList())
onCollectionSaved?.invoke(newCollection) val sessionBundle = mutableListOf<Session>()
it.tabs.forEach {
requireComponents.core.sessionManager.findSessionById(it.sessionId)?.let { session ->
sessionBundle.add(session)
}
}
launch(Dispatchers.IO) {
requireComponents.core.tabCollectionStorage.createCollection(it.name, sessionBundle)
}
} }
} }
} }

View File

@ -19,5 +19,4 @@ class Components(private val context: Context) {
val useCases by lazy { UseCases(context, core.sessionManager, search.searchEngineManager) } val useCases by lazy { UseCases(context, core.sessionManager, search.searchEngineManager) }
val utils by lazy { Utilities(context, core.sessionManager, useCases.sessionUseCases, useCases.searchUseCases) } val utils by lazy { Utilities(context, core.sessionManager, useCases.sessionUseCases, useCases.searchUseCases) }
val analytics by lazy { Analytics(context) } val analytics by lazy { Analytics(context) }
val storage by lazy { Storage(context) }
} }

View File

@ -127,8 +127,11 @@ class Core(private val context: Context) {
*/ */
val historyStorage by lazy { PlacesHistoryStorage(context) } val historyStorage by lazy { PlacesHistoryStorage(context) }
val bookmarksStorage val bookmarksStorage by lazy { PlacesBookmarksStorage(context) }
by lazy { PlacesBookmarksStorage(context) }
val tabCollectionStorage by lazy { TabCollectionStorage(context, sessionManager) }
val permissionStorage by lazy { PermissionStorage(context) }
/** /**
* Constructs a [TrackingProtectionPolicy] based on current preferences. * Constructs a [TrackingProtectionPolicy] based on current preferences.

View File

@ -12,7 +12,7 @@ import mozilla.components.feature.sitepermissions.SitePermissionsStorage
import org.mozilla.fenix.test.Mockable import org.mozilla.fenix.test.Mockable
@Mockable @Mockable
class Storage(private val context: Context) { class PermissionStorage(private val context: Context) {
private val permissionsStorage by lazy { private val permissionsStorage by lazy {
SitePermissionsStorage(context) SitePermissionsStorage(context)

View File

@ -0,0 +1,55 @@
/* 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.components
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.paging.DataSource
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.feature.tab.collections.Tab
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tab.collections.TabCollectionStorage
import org.mozilla.fenix.test.Mockable
@Mockable
class TabCollectionStorage(private val context: Context, private val sessionManager: SessionManager) {
private val collectionStorage by lazy {
TabCollectionStorage(context, sessionManager)
}
fun createCollection(title: String, sessions: List<Session>) {
collectionStorage.createCollection(title, sessions)
}
fun addTabsToCollection(tabCollection: TabCollection, sessions: List<Session>) {
collectionStorage.addTabsToCollection(tabCollection, sessions)
}
fun getCollections(limit: Int = 20): LiveData<List<TabCollection>> {
return collectionStorage.getCollections(limit)
}
fun getCollectionsPaged(): DataSource.Factory<Int, TabCollection> {
return collectionStorage.getCollectionsPaged()
}
fun removeCollection(tabCollection: TabCollection) {
collectionStorage.removeCollection(tabCollection)
}
fun removeTabFromCollection(tabCollection: TabCollection, tab: Tab) {
if (tabCollection.tabs.size == 1) {
removeCollection(tabCollection)
} else {
collectionStorage.removeTabFromCollection(tabCollection, tab)
}
}
fun renameCollection(tabCollection: TabCollection, title: String) {
collectionStorage.renameCollection(tabCollection, title)
}
}

View File

@ -164,7 +164,7 @@ class DefaultToolbarMenu(
BrowserMenuImageText( BrowserMenuImageText(
context.getString(R.string.browser_menu_save_to_collection), context.getString(R.string.browser_menu_save_to_collection),
R.drawable.ic_archive, R.drawable.ic_tab_collection,
DefaultThemeManager.resolveAttribute(R.attr.primaryText, context) DefaultThemeManager.resolveAttribute(R.attr.primaryText, context)
) { ) {
onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection) onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection)

View File

@ -13,6 +13,7 @@ import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.*
@ -68,6 +69,8 @@ import kotlin.math.roundToInt
class HomeFragment : Fragment(), CoroutineScope { class HomeFragment : Fragment(), CoroutineScope {
private val bus = ActionBusFactory.get(this) private val bus = ActionBusFactory.get(this)
private var sessionObserver: SessionManager.Observer? = null private var sessionObserver: SessionManager.Observer? = null
private var tabCollectionObserver: Observer<List<TabCollection>>? = null
private var homeMenu: HomeMenu? = null private var homeMenu: HomeMenu? = null
var deleteSessionJob: (suspend () -> Unit)? = null var deleteSessionJob: (suspend () -> Unit)? = null
@ -79,9 +82,6 @@ class HomeFragment : Fragment(), CoroutineScope {
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job get() = Dispatchers.Main + job
// TODO Remove this stub when we have the a-c version!
var storedCollections = mutableListOf<TabCollection>()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -99,7 +99,7 @@ class HomeFragment : Fragment(), CoroutineScope {
this, this,
SessionControlViewModel::class.java SessionControlViewModel::class.java
) { ) {
SessionControlViewModel(SessionControlState(listOf(), listOf(), mode)) SessionControlViewModel(SessionControlState(listOf(), setOf(), listOf(), mode))
} }
) )
@ -190,13 +190,6 @@ class HomeFragment : Fragment(), CoroutineScope {
) ?: arrayListOf()).toList() ) ?: arrayListOf()).toList()
) )
) )
getManagedEmitter<SessionControlChange>().onNext(
SessionControlChange.CollectionsChange(
(savedInstanceState.getParcelableArrayList<TabCollection>(
KEY_COLLECTIONS
) ?: arrayListOf()).toList()
)
)
} }
} }
@ -230,12 +223,17 @@ class HomeFragment : Fragment(), CoroutineScope {
emitSessionChanges() emitSessionChanges()
sessionObserver = subscribeToSessions() sessionObserver = subscribeToSessions()
tabCollectionObserver = subscribeToTabCollections()
} }
override fun onStop() { override fun onStop() {
sessionObserver?.let { sessionObserver?.let {
requireComponents.core.sessionManager.unregister(it) requireComponents.core.sessionManager.unregister(it)
} }
tabCollectionObserver?.let {
requireComponents.core.tabCollectionStorage.getCollections().removeObserver(it)
}
super.onStop() super.onStop()
} }
@ -317,13 +315,17 @@ class HomeFragment : Fragment(), CoroutineScope {
private fun handleCollectionAction(action: CollectionAction) { private fun handleCollectionAction(action: CollectionAction) {
when (action) { when (action) {
is CollectionAction.Expand -> { is CollectionAction.Expand -> {
storedCollections.find { it.id == action.collection.id }?.apply { expanded = true } getManagedEmitter<SessionControlChange>()
.onNext(SessionControlChange.ExpansionChange(action.collection, true))
} }
is CollectionAction.Collapse -> { is CollectionAction.Collapse -> {
storedCollections.find { it.id == action.collection.id }?.apply { expanded = false } getManagedEmitter<SessionControlChange>()
.onNext(SessionControlChange.ExpansionChange(action.collection, false))
} }
is CollectionAction.Delete -> { is CollectionAction.Delete -> {
storedCollections.find { it.id == action.collection.id }?.let { storedCollections.remove(it) } launch(Dispatchers.IO) {
requireComponents.core.tabCollectionStorage.removeCollection(action.collection)
}
} }
is CollectionAction.AddTab -> { is CollectionAction.AddTab -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1575") ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1575")
@ -338,17 +340,11 @@ class HomeFragment : Fragment(), CoroutineScope {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1585") ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1585")
} }
is CollectionAction.RemoveTab -> { is CollectionAction.RemoveTab -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1578") launch(Dispatchers.IO) {
requireComponents.core.tabCollectionStorage.removeTabFromCollection(action.collection, action.tab)
}
} }
} }
emitCollectionChange()
}
private fun emitCollectionChange() {
storedCollections.map { it.copy() }.let {
getManagedEmitter<SessionControlChange>().onNext(SessionControlChange.CollectionsChange(it))
}
} }
override fun onPause() { override fun onPause() {
@ -396,6 +392,14 @@ class HomeFragment : Fragment(), CoroutineScope {
return getString(resourceId) return getString(resourceId)
} }
private fun subscribeToTabCollections(): Observer<List<TabCollection>> {
val observer = Observer<List<TabCollection>> {
getManagedEmitter<SessionControlChange>().onNext(SessionControlChange.CollectionsChange(it))
}
requireComponents.core.tabCollectionStorage.getCollections().observe(this, observer)
return observer
}
private fun subscribeToSessions(): SessionManager.Observer { private fun subscribeToSessions(): SessionManager.Observer {
val observer = object : SessionManager.Observer { val observer = object : SessionManager.Observer {
override fun onSessionAdded(session: Session) { override fun onSessionAdded(session: Session) {
@ -520,6 +524,5 @@ class HomeFragment : Fragment(), CoroutineScope {
companion object { companion object {
private const val toolbarPaddingDp = 12f private const val toolbarPaddingDp = 12f
private const val KEY_TABS = "tabs" private const val KEY_TABS = "tabs"
private const val KEY_COLLECTIONS = "collections"
} }
} }

View File

@ -43,7 +43,7 @@ class SessionBottomSheetFragment : BottomSheetDialogFragment(), LayoutContainer
view.current_session_card_title.text = getCardTitle() view.current_session_card_title.text = getCardTitle()
view.current_session_card_tab_list.text = getTabTitles() view.current_session_card_tab_list.text = getTabTitles()
view.archive_session_button.apply { view.archive_session_button.apply {
val drawable = ContextCompat.getDrawable(context!!, R.drawable.ic_archive) val drawable = ContextCompat.getDrawable(context!!, R.drawable.ic_tab_collection)
drawable?.setColorFilter( drawable?.setColorFilter(
ContextCompat.getColor( ContextCompat.getColor(
context!!, context!!,

View File

@ -28,6 +28,7 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingPr
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingSectionHeaderViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingSectionHeaderViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingThemePickerViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingThemePickerViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingTrackingProtectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingTrackingProtectionViewHolder
import mozilla.components.feature.tab.collections.Tab as ComponentTab
import java.lang.IllegalStateException import java.lang.IllegalStateException
sealed class AdapterItem { sealed class AdapterItem {
@ -42,7 +43,11 @@ sealed class AdapterItem {
object CollectionHeader : AdapterItem() object CollectionHeader : AdapterItem()
object NoCollectionMessage : AdapterItem() object NoCollectionMessage : AdapterItem()
data class CollectionItem(val collection: TabCollection) : AdapterItem() data class CollectionItem(val collection: TabCollection) : AdapterItem()
data class TabInCollectionItem(val collection: TabCollection, val tab: Tab, val isLastTab: Boolean) : AdapterItem() data class TabInCollectionItem(
val collection: TabCollection,
val tab: ComponentTab,
val isLastTab: Boolean
) : AdapterItem()
object OnboardingHeader : AdapterItem() object OnboardingHeader : AdapterItem()
data class OnboardingSectionHeader(val labelBuilder: (Context) -> String) : AdapterItem() data class OnboardingSectionHeader(val labelBuilder: (Context) -> String) : AdapterItem()
@ -82,9 +87,11 @@ class SessionControlAdapter(
private var items: List<AdapterItem> = listOf() private var items: List<AdapterItem> = listOf()
private lateinit var job: Job private lateinit var job: Job
private lateinit var expandedCollections: Set<Long>
fun reloadData(items: List<AdapterItem>) { fun reloadData(items: List<AdapterItem>, expandedCollections: Set<Long>) {
this.items = items this.items = items
this.expandedCollections = expandedCollections
notifyDataSetChanged() notifyDataSetChanged()
} }
@ -138,9 +145,10 @@ class SessionControlAdapter(
is TabViewHolder -> holder.bindSession( is TabViewHolder -> holder.bindSession(
(items[position] as AdapterItem.TabItem).tab (items[position] as AdapterItem.TabItem).tab
) )
is CollectionViewHolder -> holder.bindSession( is CollectionViewHolder -> {
(items[position] as AdapterItem.CollectionItem).collection val collection = (items[position] as AdapterItem.CollectionItem).collection
) holder.bindSession(collection, expandedCollections.contains(collection.id))
}
is TabInCollectionViewHolder -> { is TabInCollectionViewHolder -> {
val item = items[position] as AdapterItem.TabInCollectionItem val item = items[position] as AdapterItem.TabInCollectionItem
holder.bindSession(item.collection, item.tab, item.isLastTab) holder.bindSession(item.collection, item.tab, item.isLastTab)

View File

@ -4,6 +4,7 @@
package org.mozilla.fenix.home.sessioncontrol package org.mozilla.fenix.home.sessioncontrol
import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Parcelable import android.os.Parcelable
import android.view.ViewGroup import android.view.ViewGroup
@ -12,6 +13,10 @@ import io.reactivex.Observer
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.mvi.ViewState
import org.mozilla.fenix.mvi.Change import org.mozilla.fenix.mvi.Change
import mozilla.components.browser.session.Session
import mozilla.components.feature.tab.collections.TabCollection as ACTabCollection
import mozilla.components.feature.tab.collections.Tab as ComponentTab
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.mvi.Action import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.UIComponent import org.mozilla.fenix.mvi.UIComponent
@ -49,15 +54,6 @@ data class Tab(
val thumbnail: Bitmap? = null val thumbnail: Bitmap? = null
) : Parcelable ) : Parcelable
@Parcelize
data class TabCollection(
val id: Int,
val title: String,
val tabs: MutableList<Tab>,
val iconColor: Int = 0,
var expanded: Boolean = false
) : Parcelable
sealed class Mode { sealed class Mode {
object Normal : Mode() object Normal : Mode()
object Private : Mode() object Private : Mode()
@ -66,10 +62,13 @@ sealed class Mode {
data class SessionControlState( data class SessionControlState(
val tabs: List<Tab>, val tabs: List<Tab>,
val expandedCollections: Set<Long>,
val collections: List<TabCollection>, val collections: List<TabCollection>,
val mode: Mode val mode: Mode
) : ViewState ) : ViewState
typealias TabCollection = ACTabCollection
sealed class TabAction : Action { sealed class TabAction : Action {
data class SaveTabGroup(val selectedTabSessionId: String?) : TabAction() data class SaveTabGroup(val selectedTabSessionId: String?) : TabAction()
object Add : TabAction() object Add : TabAction()
@ -89,7 +88,7 @@ sealed class CollectionAction : Action {
data class Rename(val collection: TabCollection) : CollectionAction() data class Rename(val collection: TabCollection) : CollectionAction()
data class OpenTabs(val collection: TabCollection) : CollectionAction() data class OpenTabs(val collection: TabCollection) : CollectionAction()
data class ShareTabs(val collection: TabCollection) : CollectionAction() data class ShareTabs(val collection: TabCollection) : CollectionAction()
data class RemoveTab(val collection: TabCollection, val tab: Tab) : CollectionAction() data class RemoveTab(val collection: TabCollection, val tab: ComponentTab) : CollectionAction()
} }
sealed class OnboardingAction : Action { sealed class OnboardingAction : Action {
@ -118,17 +117,33 @@ sealed class SessionControlChange : Change {
data class TabsChange(val tabs: List<Tab>) : SessionControlChange() data class TabsChange(val tabs: List<Tab>) : SessionControlChange()
data class ModeChange(val mode: Mode) : SessionControlChange() data class ModeChange(val mode: Mode) : SessionControlChange()
data class CollectionsChange(val collections: List<TabCollection>) : SessionControlChange() data class CollectionsChange(val collections: List<TabCollection>) : SessionControlChange()
data class ExpansionChange(val collection: TabCollection, val expand: Boolean) : SessionControlChange()
} }
class SessionControlViewModel( class SessionControlViewModel(
initialState: SessionControlState initialState: SessionControlState
) : UIComponentViewModelBase<SessionControlState, SessionControlChange>(initialState, reducer) { ) : UIComponentViewModelBase<SessionControlState, SessionControlChange>(initialState, reducer) {
companion object { companion object {
fun getSessionFromTab(context: Context, tab: Tab): Session? {
return context.components.core.sessionManager.findSessionById(tab.sessionId)
}
val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change -> val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change ->
when (change) { when (change) {
is SessionControlChange.CollectionsChange -> state.copy(collections = change.collections) is SessionControlChange.CollectionsChange -> state.copy(collections = change.collections)
is SessionControlChange.TabsChange -> state.copy(tabs = change.tabs) is SessionControlChange.TabsChange -> state.copy(tabs = change.tabs)
is SessionControlChange.ModeChange -> state.copy(mode = change.mode) is SessionControlChange.ModeChange -> state.copy(mode = change.mode)
is SessionControlChange.ExpansionChange -> {
val newExpandedCollection = state.expandedCollections.toMutableSet()
if (change.expand) {
newExpandedCollection.add(change.collection.id)
} else {
newExpandedCollection.remove(change.collection.id)
}
state.copy(expandedCollections = newExpandedCollection)
}
} }
} }
} }

View File

@ -16,7 +16,11 @@ import org.mozilla.fenix.mvi.UIView
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.BuildConfig
private fun normalModeAdapterItems(tabs: List<Tab>, collections: List<TabCollection>): List<AdapterItem> { private fun normalModeAdapterItems(
tabs: List<Tab>,
collections: List<TabCollection>,
expandedCollections: Set<Long>
): List<AdapterItem> {
val items = mutableListOf<AdapterItem>() val items = mutableListOf<AdapterItem>()
items.add(AdapterItem.TabHeader(false, tabs.isNotEmpty())) items.add(AdapterItem.TabHeader(false, tabs.isNotEmpty()))
@ -33,9 +37,9 @@ private fun normalModeAdapterItems(tabs: List<Tab>, collections: List<TabCollect
if (collections.isNotEmpty()) { if (collections.isNotEmpty()) {
// If the collection is expanded, we want to add all of its tabs beneath it in the adapter // If the collection is expanded, we want to add all of its tabs beneath it in the adapter
collections.reversed().map(AdapterItem::CollectionItem).forEach { collections.map(AdapterItem::CollectionItem).forEach {
items.add(it) items.add(it)
if (it.collection.expanded) { if (it.collection.isExpanded(expandedCollections)) {
items.addAll(collectionTabItems(it.collection)) items.addAll(collectionTabItems(it.collection))
} }
} }
@ -76,14 +80,18 @@ private fun onboardingAdapterItems(): List<AdapterItem> = listOf(
) )
private fun SessionControlState.toAdapterList(): List<AdapterItem> = when (mode) { private fun SessionControlState.toAdapterList(): List<AdapterItem> = when (mode) {
is Mode.Normal -> normalModeAdapterItems(tabs, collections) is Mode.Normal -> normalModeAdapterItems(tabs, collections, expandedCollections)
is Mode.Private -> privateModeAdapterItems(tabs) is Mode.Private -> privateModeAdapterItems(tabs)
is Mode.Onboarding -> onboardingAdapterItems() is Mode.Onboarding -> onboardingAdapterItems()
} }
private fun collectionTabItems(collection: TabCollection) = collection.tabs.mapIndexed { index, tab -> private fun collectionTabItems(collection: TabCollection) = collection.tabs.mapIndexed { index, tab ->
AdapterItem.TabInCollectionItem(collection, tab, index == collection.tabs.lastIndex) AdapterItem.TabInCollectionItem(collection, tab, index == collection.tabs.lastIndex)
} }
private fun TabCollection.isExpanded(expandedCollections: Set<Long>): Boolean {
return expandedCollections.contains(this.id)
}
class SessionControlUIView( class SessionControlUIView(
container: ViewGroup, container: ViewGroup,
@ -101,6 +109,7 @@ class SessionControlUIView(
.findViewById(R.id.home_component) .findViewById(R.id.home_component)
private val sessionControlAdapter = SessionControlAdapter(actionEmitter) private val sessionControlAdapter = SessionControlAdapter(actionEmitter)
private var expandedCollections = setOf<Long>()
init { init {
view.apply { view.apply {
@ -117,8 +126,8 @@ class SessionControlUIView(
} }
override fun updateView() = Consumer<SessionControlState> { override fun updateView() = Consumer<SessionControlState> {
sessionControlAdapter.reloadData(it.toAdapterList()) sessionControlAdapter.reloadData(it.toAdapterList(), it.expandedCollections)
expandedCollections = it.expandedCollections
// There is a current bug in the combination of MotionLayout~alhpa4 and RecyclerView where it doesn't think // There is a current bug in the combination of MotionLayout~alhpa4 and RecyclerView where it doesn't think
// it has to redraw itself. For some reason calling scrollBy forces this to happen every time // it has to redraw itself. For some reason calling scrollBy forces this to happen every time
// https://stackoverflow.com/a/42549611 // https://stackoverflow.com/a/42549611

View File

@ -21,6 +21,7 @@ import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import org.mozilla.fenix.DefaultThemeManager import org.mozilla.fenix.DefaultThemeManager
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.urlToTrimmedHost
import org.mozilla.fenix.home.sessioncontrol.CollectionAction import org.mozilla.fenix.home.sessioncontrol.CollectionAction
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
import org.mozilla.fenix.home.sessioncontrol.TabCollection import org.mozilla.fenix.home.sessioncontrol.TabCollection
@ -40,6 +41,7 @@ class CollectionViewHolder(
get() = Dispatchers.IO + job get() = Dispatchers.IO + job
private lateinit var collection: TabCollection private lateinit var collection: TabCollection
private var expanded = false
private var state = CollectionState.Collapsed private var state = CollectionState.Collapsed
private var collectionMenu: CollectionItemMenu private var collectionMenu: CollectionItemMenu
@ -78,8 +80,9 @@ class CollectionViewHolder(
) )
} }
fun bindSession(collection: TabCollection) { fun bindSession(collection: TabCollection, expanded: Boolean) {
this.collection = collection this.collection = collection
this.expanded = expanded
updateCollectionUI() updateCollectionUI()
} }
@ -89,7 +92,7 @@ class CollectionViewHolder(
var hostNameList = listOf<String>() var hostNameList = listOf<String>()
collection.tabs.forEach { collection.tabs.forEach {
hostNameList += it.hostname.capitalize() hostNameList += it.url.urlToTrimmedHost().capitalize()
} }
var tabsDisplayed = 0 var tabsDisplayed = 0
@ -106,7 +109,7 @@ class CollectionViewHolder(
view.collection_description.text = titleList view.collection_description.text = titleList
if (collection.expanded) { if (expanded) {
(view.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = 0 (view.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = 0
collection_title.setPadding(0, 0, 0, EXPANDED_PADDING) collection_title.setPadding(0, 0, 0, EXPANDED_PADDING)
view.background = ContextCompat.getDrawable(view.context, R.drawable.rounded_top_corners) view.background = ContextCompat.getDrawable(view.context, R.drawable.rounded_top_corners)

View File

@ -23,12 +23,13 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getColorFromAttr import org.mozilla.fenix.ext.getColorFromAttr
import org.mozilla.fenix.ext.increaseTapArea import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.urlToTrimmedHost
import org.mozilla.fenix.home.sessioncontrol.CollectionAction import org.mozilla.fenix.home.sessioncontrol.CollectionAction
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
import org.mozilla.fenix.home.sessioncontrol.Tab
import org.mozilla.fenix.home.sessioncontrol.TabCollection import org.mozilla.fenix.home.sessioncontrol.TabCollection
import org.mozilla.fenix.home.sessioncontrol.onNext import org.mozilla.fenix.home.sessioncontrol.onNext
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import mozilla.components.feature.tab.collections.Tab as ComponentTab
class TabInCollectionViewHolder( class TabInCollectionViewHolder(
val view: View, val view: View,
@ -42,7 +43,7 @@ class TabInCollectionViewHolder(
lateinit var collection: TabCollection lateinit var collection: TabCollection
private set private set
lateinit var tab: Tab lateinit var tab: ComponentTab
private set private set
var isLastTab = false var isLastTab = false
@ -66,7 +67,7 @@ class TabInCollectionViewHolder(
} }
} }
fun bindSession(collection: TabCollection, tab: Tab, isLastTab: Boolean) { fun bindSession(collection: TabCollection, tab: ComponentTab, isLastTab: Boolean) {
this.collection = collection this.collection = collection
this.tab = tab this.tab = tab
this.isLastTab = isLastTab this.isLastTab = isLastTab
@ -74,7 +75,7 @@ class TabInCollectionViewHolder(
} }
private fun updateTabUI() { private fun updateTabUI() {
collection_tab_hostname.text = tab.hostname collection_tab_hostname.text = tab.url.urlToTrimmedHost()
collection_tab_title.text = tab.title collection_tab_title.text = tab.title
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
val bitmap = collection_tab_icon.context.components.utils.icons val bitmap = collection_tab_icon.context.components.utils.icons

View File

@ -47,7 +47,7 @@ import org.mozilla.fenix.ext.allowUndo
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.share import org.mozilla.fenix.ext.share
import org.mozilla.fenix.ext.urlToHost import org.mozilla.fenix.ext.urlToTrimmedHost
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.mvi.getManagedEmitter import org.mozilla.fenix.mvi.getManagedEmitter
@ -238,10 +238,12 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler, AccountObserve
is BookmarkAction.Delete -> { is BookmarkAction.Delete -> {
val components = context?.applicationContext?.components!! val components = context?.applicationContext?.components!!
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.Change(currentRoot - it.item.guid)) getManagedEmitter<BookmarkChange>()
.onNext(BookmarkChange.Change(currentRoot - it.item.guid))
CoroutineScope(Main).allowUndo( CoroutineScope(Main).allowUndo(
view!!, getString(R.string.bookmark_deletion_snackbar_message, it.item.url.urlToHost()), view!!,
getString(R.string.bookmark_deletion_snackbar_message, it.item.url.urlToTrimmedHost()),
getString(R.string.bookmark_undo_deletion), { refreshBookmarks(components) } getString(R.string.bookmark_undo_deletion), { refreshBookmarks(components) }
) { ) {
components.core.bookmarksStorage.deleteNode(it.item.guid) components.core.bookmarksStorage.deleteNode(it.item.guid)

View File

@ -53,7 +53,8 @@ class SitePermissionsDetailsExceptionsFragment : PreferenceFragmentCompat(), Cor
super.onResume() super.onResume()
launch(IO) { launch(IO) {
val context = requireContext() val context = requireContext()
sitePermissions = requireNotNull(context.components.storage.findSitePermissionsBy(sitePermissions.origin)) sitePermissions =
requireNotNull(context.components.core.permissionStorage.findSitePermissionsBy(sitePermissions.origin))
launch(Main) { launch(Main) {
bindCategoryPhoneFeatures() bindCategoryPhoneFeatures()
} }
@ -112,7 +113,7 @@ class SitePermissionsDetailsExceptionsFragment : PreferenceFragmentCompat(), Cor
private fun clearSitePermissions() { private fun clearSitePermissions() {
launch(IO) { launch(IO) {
requireContext().components.storage.deleteSitePermissions(sitePermissions) requireContext().components.core.permissionStorage.deleteSitePermissions(sitePermissions)
launch(Main) { launch(Main) {
Navigation.findNavController(requireNotNull(view)).popBackStack() Navigation.findNavController(requireNotNull(view)).popBackStack()
} }

View File

@ -70,7 +70,7 @@ class SitePermissionsExceptionsFragment : Fragment(), View.OnClickListener, Coro
recyclerView = rootView.findViewById(R.id.exceptions) recyclerView = rootView.findViewById(R.id.exceptions)
recyclerView.layoutManager = LinearLayoutManager(requireContext()) recyclerView.layoutManager = LinearLayoutManager(requireContext())
val sitePermissionsPaged = requireContext().components.storage.getSitePermissionsPaged() val sitePermissionsPaged = requireContext().components.core.permissionStorage.getSitePermissionsPaged()
val adapter = ExceptionsAdapter(this) val adapter = ExceptionsAdapter(this)
val liveData = LivePagedListBuilder(sitePermissionsPaged, MAX_ITEMS_PER_PAGE).build() val liveData = LivePagedListBuilder(sitePermissionsPaged, MAX_ITEMS_PER_PAGE).build()
@ -124,7 +124,7 @@ class SitePermissionsExceptionsFragment : Fragment(), View.OnClickListener, Coro
private fun deleteAllSitePermissions() { private fun deleteAllSitePermissions() {
launch(IO) { launch(IO) {
requireContext().components.storage.deleteAllSitePermissions() requireContext().components.core.permissionStorage.deleteAllSitePermissions()
launch(Main) { launch(Main) {
showEmptyListMessage() showEmptyListMessage()
} }

View File

@ -172,7 +172,7 @@ class SitePermissionsManageExceptionsPhoneFeatureFragment : Fragment(), Coroutin
PhoneFeature.NOTIFICATION -> sitePermissions.copy(notification = status) PhoneFeature.NOTIFICATION -> sitePermissions.copy(notification = status)
} }
launch(IO) { launch(IO) {
requireContext().components.storage.updateSitePermissions(updatedSitePermissions) requireContext().components.core.permissionStorage.updateSitePermissions(updatedSitePermissions)
} }
} }
} }

View File

@ -60,10 +60,11 @@ class QuickSettingsComponent(
PhoneFeature.MICROPHONE -> microphone = microphone.toggle() PhoneFeature.MICROPHONE -> microphone = microphone.toggle()
PhoneFeature.NOTIFICATION -> notification = notification.toggle() PhoneFeature.NOTIFICATION -> notification = notification.toggle()
} }
context.components.storage.addSitePermissionException(origin, location, notification, microphone, camera) context.components.core.permissionStorage
.addSitePermissionException(origin, location, notification, microphone, camera)
} else { } else {
val updatedSitePermissions = sitePermissions.toggle(featurePhone) val updatedSitePermissions = sitePermissions.toggle(featurePhone)
context.components.storage.updateSitePermissions(updatedSitePermissions) context.components.core.permissionStorage.updateSitePermissions(updatedSitePermissions)
updatedSitePermissions updatedSitePermissions
} }
} }

View File

@ -245,7 +245,7 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment(), CoroutineSco
launch { launch {
val host = session.url.toUri()?.host val host = session.url.toUri()?.host
val sitePermissions: SitePermissions? = host?.let { val sitePermissions: SitePermissions? = host?.let {
val storage = requireContext().components.storage val storage = requireContext().components.core.permissionStorage
storage.findSitePermissionsBy(it) storage.findSitePermissionsBy(it)
} }
launch(Dispatchers.Main) { launch(Dispatchers.Main) {

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,20 @@
<?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/. -->
<vector android:height="24dp" android:viewportHeight="20"
android:viewportWidth="20" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?primaryText" android:fillType="nonZero"
android:pathData="M18,6L2,6L2,17C2,17.5523 2.4477,18 3,18L17,18C17.5523,18 18,17.5523 18,17L18,6ZM16.7908,4L15.333,2.3401C15.1432,2.1239 14.8694,2 14.5817,2L5.8284,2C5.5632,2 5.3089,2.1054 5.1213,2.2929L3.4142,4L16.7908,4ZM0,17L0,5.8284C0,5.0328 0.3161,4.2697 0.8787,3.7071L3.7071,0.8787C4.2697,0.3161 5.0328,0 5.8284,0L14.5817,0C15.4448,0 16.2662,0.3718 16.8358,1.0203L19.2541,3.7739C19.7349,4.3213 20,5.025 20,5.7536L20,17C20,18.6569 18.6569,20 17,20L3,20C1.3431,20 0,18.6569 0,17Z"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="?primaryText" android:fillType="nonZero"
android:pathData="M14.95,8.5L5.05,8.5C4.7462,8.5 4.5,8.7239 4.5,9C4.5,9.2761 4.7462,9.5 5.05,9.5L14.95,9.5C15.2538,9.5 15.5,9.2761 15.5,9C15.5,8.7239 15.2538,8.5 14.95,8.5Z"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="?primaryText" android:fillType="nonZero"
android:pathData="M14.95,14.5L5.05,14.5C4.7462,14.5 4.5,14.7239 4.5,15C4.5,15.2761 4.7462,15.5 5.05,15.5L14.95,15.5C15.2538,15.5 15.5,15.2761 15.5,15C15.5,14.7239 15.2538,14.5 14.95,14.5Z"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="?primaryText" android:fillType="nonZero"
android:pathData="M14.95,11.5L5.05,11.5C4.7462,11.5 4.5,11.7239 4.5,12C4.5,12.2761 4.7462,12.5 5.05,12.5L14.95,12.5C15.2538,12.5 15.5,12.2761 15.5,12C15.5,11.7239 15.2538,11.5 14.95,11.5Z"
android:strokeColor="#00000000" android:strokeWidth="1"/>
</vector>

View File

@ -27,7 +27,7 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:tint="@null" android:tint="@null"
android:src="@drawable/ic_archive" android:src="@drawable/ic_tab_collection"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toTopOf="parent"/>

View File

@ -13,7 +13,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?foundation" android:background="?foundation"
android:drawableStart="@drawable/ic_archive" android:drawableStart="@drawable/ic_tab_collection"
android:drawablePadding="14dp" android:drawablePadding="14dp"
android:drawableTint="?accent" android:drawableTint="?accent"
android:paddingStart="20dp" android:paddingStart="20dp"

View File

@ -16,7 +16,7 @@
android:id="@+id/no_collection_header" android:id="@+id/no_collection_header"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawableEnd="@drawable/ic_archive" android:drawableEnd="@drawable/ic_tab_collection"
android:drawableTint="?primaryText" android:drawableTint="?primaryText"
android:drawablePadding="8dp" android:drawablePadding="8dp"
android:text="@string/no_collections_header" android:text="@string/no_collections_header"

View File

@ -23,7 +23,7 @@
android:layout_gravity="center" android:layout_gravity="center"
android:clickable="false" android:clickable="false"
android:drawableTint="?foundation" android:drawableTint="?foundation"
android:drawableStart="@drawable/ic_archive" android:drawableStart="@drawable/ic_tab_collection"
android:drawablePadding="8dp" android:drawablePadding="8dp"
android:focusable="false" android:focusable="false"
android:gravity="center" android:gravity="center"

View File

@ -100,7 +100,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?foundation" android:background="?foundation"
android:drawableStart="@drawable/ic_archive" android:drawableStart="@drawable/ic_tab_collection"
android:drawablePadding="14dp" android:drawablePadding="14dp"
android:drawableTint="?accent" android:drawableTint="?accent"
android:paddingStart="20dp" android:paddingStart="20dp"

View File

@ -27,7 +27,7 @@
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:tint="@null" android:tint="@null"
android:src="@drawable/ic_archive" android:src="@drawable/ic_tab_collection"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toTopOf="parent"/>

View File

@ -20,5 +20,4 @@ class TestComponents(private val context: Context) : Components(context) {
) )
} }
override val analytics by lazy { Analytics(context) } override val analytics by lazy { Analytics(context) }
override val storage by lazy { Storage(context) }
} }

View File

@ -109,6 +109,7 @@ object Deps {
const val mozilla_feature_session_bundling = "org.mozilla.components:feature-session-bundling:${Versions.mozilla_android_components}" const val mozilla_feature_session_bundling = "org.mozilla.components:feature-session-bundling:${Versions.mozilla_android_components}"
const val mozilla_feature_site_permissions = "org.mozilla.components:feature-sitepermissions:${Versions.mozilla_android_components}" const val mozilla_feature_site_permissions = "org.mozilla.components:feature-sitepermissions:${Versions.mozilla_android_components}"
const val mozilla_feature_readerview = "org.mozilla.components:feature-readerview:${Versions.mozilla_android_components}" const val mozilla_feature_readerview = "org.mozilla.components:feature-readerview:${Versions.mozilla_android_components}"
const val mozilla_feature_tab_collections = "org.mozilla.components:feature-tab-collections:${Versions.mozilla_android_components}"
const val mozilla_service_firefox_accounts = "org.mozilla.components:service-firefox-accounts:${Versions.mozilla_android_components}" const val mozilla_service_firefox_accounts = "org.mozilla.components:service-firefox-accounts:${Versions.mozilla_android_components}"
const val mozilla_service_fretboard = "org.mozilla.components:service-fretboard:${Versions.mozilla_android_components}" const val mozilla_service_fretboard = "org.mozilla.components:service-fretboard:${Versions.mozilla_android_components}"