Commit 9533a255 authored by MozLando's avatar MozLando
Browse files

Merge #6866



6866: Close #6836: Refactor feature-syncedtabs to use interactor-presenter pattern r=csadilek a=jonalmeida

We've renamed the original `SyncedTabsFeature` to `SyncedTabsStorage` since it breaks some of our conventions of how we use "feature" components.

A new `SyncedTabsFeature` moves the logic placed in Reference Browser into an interactor/presenter architecture to make it easier for other consumers to get this for free.



Co-authored-by: default avatarJonathan Almeida <jalmeida@mozilla.com>
parents ee10c758 a899f121
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@ import mozilla.components.concept.storage.Storage
import mozilla.components.concept.sync.SyncableStore
import mozilla.components.support.base.log.logger.Logger
import mozilla.appservices.remotetabs.RemoteTabsProvider
import mozilla.components.concept.sync.Device
import mozilla.appservices.remotetabs.SyncAuthInfo as RustSyncAuthInfo
import mozilla.components.concept.sync.SyncAuthInfo
import mozilla.components.concept.sync.SyncStatus
@@ -145,6 +146,14 @@ data class Tab(
    }
}

/**
 * A synced device and the list of tabs.
 */
data class SyncedDeviceTabs(
    val device: Device,
    val tabs: List<Tab>
)

/**
 * A Tab history entry.
 */
+1 −1
Original line number Diff line number Diff line
@@ -191,7 +191,7 @@ internal class AutoPushObserver(
        val rawEvent = message ?: return

        accountManager.withConstellation {
            it.processRawEventAsync(String(rawEvent))
            processRawEventAsync(String(rawEvent))
        }
    }

+1 −1
Original line number Diff line number Diff line
@@ -49,7 +49,7 @@ class SendTabFeatureKtTest {
    @Test
    fun `block is executed only account is available`() {
        val accountManager: FxaAccountManager = mock()
        val block: (DeviceConstellation) -> Unit = mock()
        val block: DeviceConstellation.() -> Unit = mock()
        val account: OAuthAccount = mock()
        val constellation: DeviceConstellation = mock()

+6 −0
Original line number Diff line number Diff line
@@ -27,6 +27,12 @@ android {
    }
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
    kotlinOptions.freeCompilerArgs += [
            "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi"
    ]
}

dependencies {
    implementation project(':service-firefox-accounts')
    implementation project(':browser-icons')
+55 −67
Original line number Diff line number Diff line
@@ -4,81 +4,69 @@

package mozilla.components.feature.syncedtabs

import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.storage.sync.RemoteTabsStorage
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.Dispatchers
import mozilla.components.browser.storage.sync.Tab
import mozilla.components.browser.storage.sync.TabEntry
import mozilla.components.concept.sync.Device
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.feature.syncedtabs.controller.DefaultController
import mozilla.components.feature.syncedtabs.controller.SyncedTabsController
import mozilla.components.feature.syncedtabs.interactor.DefaultInteractor
import mozilla.components.feature.syncedtabs.interactor.SyncedTabsInteractor
import mozilla.components.feature.syncedtabs.presenter.DefaultPresenter
import mozilla.components.feature.syncedtabs.presenter.SyncedTabsPresenter
import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.service.fxa.manager.ext.withConstellation
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.support.base.feature.LifecycleAwareFeature
import kotlin.coroutines.CoroutineContext

/**
 * A feature that listens to the [BrowserStore] changes to update the local remote tabs state
 * in [RemoteTabsStorage].
 * Feature implementation that will keep a [SyncedTabsView] notified with other synced device tabs for
 * the Firefox Sync account.
 *
 * @param storage The synced tabs storage that stores the current device's and remote device tabs.
 * @param accountManager Firefox Account Manager that holds a Firefox Sync account.
 * @param view An implementor of [SyncedTabsView] that will be notified of changes.
 * @param lifecycleOwner Android Lifecycle Owner to bind observers onto.
 * @param coroutineContext A coroutine context that can be used to perform work off the main thread.
 * @param onTabClicked Invoked when a tab is selected by the user on the [SyncedTabsView].
 * @param controller See [SyncedTabsController].
 * @param presenter See [SyncedTabsPresenter].
 * @param interactor See [SyncedTabsInteractor].
 */
@ExperimentalCoroutinesApi
class SyncedTabsFeature(
    private val accountManager: FxaAccountManager,
    private val store: BrowserStore,
    private val tabsStorage: RemoteTabsStorage = RemoteTabsStorage()
) {
    private var scope: CoroutineScope? = null
    storage: SyncedTabsStorage,
    accountManager: FxaAccountManager,
    view: SyncedTabsView,
    lifecycleOwner: LifecycleOwner,
    coroutineContext: CoroutineContext = Dispatchers.IO,
    onTabClicked: (Tab) -> Unit,
    controller: SyncedTabsController = DefaultController(
        storage,
        accountManager,
        view,
        coroutineContext
    ),
    private val presenter: SyncedTabsPresenter = DefaultPresenter(
        controller,
        accountManager,
        view,
        lifecycleOwner
    ),
    private val interactor: SyncedTabsInteractor = DefaultInteractor(
        accountManager,
        view,
        coroutineContext,
        onTabClicked
    )
) : LifecycleAwareFeature {

    /**
     * Start listening to browser store changes.
     */
    fun start() {
        scope = store.flowScoped { flow ->
            flow.ifChanged { it.tabs }.map { state ->
                // TO-DO: https://github.com/mozilla-mobile/android-components/issues/5178
                val lastUsed = 0L
                // TO-DO: https://github.com/mozilla-mobile/android-components/issues/5179
                val iconUrl = null
                state.tabs.filter { !it.content.private }.map { tab ->
                    // TO-DO: https://github.com/mozilla-mobile/android-components/issues/1340
                    val history = listOf(TabEntry(tab.content.title, tab.content.url, iconUrl))
                    Tab(history, 0, lastUsed)
                }
            }.collect { tabs ->
                tabsStorage.store(tabs)
            }
        }
    }

    /**
     * Stop listening to browser store changes.
     */
    fun stop() {
        scope?.cancel()
    }

    /**
     * Get the list of remote tabs.
     */
    suspend fun getSyncedTabs(): Map<Device, List<Tab>> {
        val otherDevices = syncClients() ?: return emptyMap()
        return tabsStorage.getAll().mapNotNull { (client, tabs) ->
            val fxaDevice = otherDevices.find { it.id == client.id }
            fxaDevice?.let { fxaDevice to tabs }
        }.toMap()
    override fun start() {
        presenter.start()
        interactor.start()
    }

    /**
     * List of synced devices.
     */
    @VisibleForTesting
    internal fun syncClients(): List<Device>? {
        accountManager.withConstellation { constellation ->
            return constellation.state()?.otherDevices
        }
        return null
    override fun stop() {
        presenter.stop()
        interactor.stop()
    }
}
Loading