Commit 6835a19f authored by Jonathan Almeida's avatar Jonathan Almeida Committed by Jonathan Almeida
Browse files

Closes #3769: Add SendTabFeature with push support

parent 88143495
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -205,6 +205,11 @@ class AutoPushFeature(

    /**
     * Returns subscription information for the push type if available.
     *
     * Implementation notes: We need to connect this to the device constellation so that we update our subscriptions
     * when notified by FxA. See [#3859][0].
     *
     * [0]: https://github.com/mozilla-mobile/android-components/issues/3859
     */
    fun unsubscribeForType(type: PushType) {
        DeliveryManager.with(connection) {
@@ -231,6 +236,11 @@ class AutoPushFeature(
    /**
     * Deletes the registration token locally so that it forces the service to get a new one the
     * next time hits it's messaging server.
     *
     * Implementation notes: This shouldn't need to be used unless we're certain. When we introduce
     * [a polling service][0] to check if endpoints are expired, we would invoke this.
     *
     * [0]: https://github.com/mozilla-mobile/android-components/issues/3173
     */
    fun forceRegistrationRenewal() {
        // Remove the cached token we have.
+3 −0
Original line number Diff line number Diff line
@@ -31,7 +31,10 @@ dependencies {
    implementation project(':service-firefox-accounts')
    implementation project(':support-ktx')
    implementation project(':support-base')
    implementation project(':concept-push')
    implementation project(':feature-push')

    implementation Dependencies.androidx_work_runtime
    implementation Dependencies.kotlin_stdlib
    implementation Dependencies.kotlin_coroutines

+140 −0
Original line number Diff line number Diff line
/*
 * 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 mozilla.components.feature.sendtab

import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import mozilla.components.concept.push.Bus
import mozilla.components.concept.push.PushService
import mozilla.components.concept.sync.AccountObserver as SyncAccountObserver
import mozilla.components.concept.sync.Device
import mozilla.components.concept.sync.DeviceConstellation
import mozilla.components.concept.sync.DeviceEvent
import mozilla.components.concept.sync.DeviceEventsObserver
import mozilla.components.concept.sync.DevicePushSubscription
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.TabData
import mozilla.components.feature.push.AutoPushFeature
import mozilla.components.feature.push.AutoPushSubscription
import mozilla.components.feature.push.PushSubscriptionObserver
import mozilla.components.feature.push.PushType
import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.support.base.log.logger.Logger

/**
 * A feature that uses the [FxaAccountManager] to send and receive tabs with optional push support
 * for receiving tabs from the [AutoPushFeature] and a [PushService].
 *
 * If the push components are not used, the feature can still function while tabs would only be
 * received when refreshing the device state.
 *
 * @param accountManager Firefox account manager.
 * @param pushFeature The [AutoPushFeature] if that is setup for observing push events.
 * @param owner Android lifecycle owner for the observers. Defaults to the [ProcessLifecycleOwner]
 * so that we can always observe events throughout the application lifecycle.
 * @param autoPause whether or not the observer should automatically be
 * paused/resumed with the bound lifecycle.
 * @param onTabsReceived the callback invoked with new tab(s) are received.
 */
class SendTabFeature(
    accountManager: FxaAccountManager,
    pushFeature: AutoPushFeature? = null,
    owner: LifecycleOwner = ProcessLifecycleOwner.get(),
    autoPause: Boolean = false,
    onTabsReceived: (Device?, List<TabData>) -> Unit
) {
    init {
        val accountObserver = AccountObserver(pushFeature)
        val pushObserver = PushObserver(accountManager)
        val deviceObserver = DeviceObserver(onTabsReceived)

        // Always observe the account for device events.
        accountManager.registerForDeviceEvents(deviceObserver, owner, autoPause)

        pushFeature?.apply {
            registerForPushMessages(PushType.Services, pushObserver, owner, autoPause)
            registerForSubscriptions(pushObserver, owner, autoPause)

            // observe the account only if we have the push feature (service is optional)
            accountManager.register(accountObserver, owner, autoPause)
        }
    }
}

internal class PushObserver(
    private val accountManager: FxaAccountManager
) : Bus.Observer<PushType, String>, PushSubscriptionObserver {
    private val logger = Logger("PushObserver")

    override fun onSubscriptionAvailable(subscription: AutoPushSubscription) {
        logger.debug("Received new push subscription from $subscription.type")

        if (subscription.type == PushType.Services) {
            accountManager.withConstellation {
                it.setDevicePushSubscriptionAsync(
                    DevicePushSubscription(
                        endpoint = subscription.endpoint,
                        publicKey = subscription.publicKey,
                        authKey = subscription.authKey
                    )
                )
            }
        }
    }

    override fun onEvent(type: PushType, message: String) {
        logger.debug("Received new push message for $type")

        accountManager.withConstellation {
            it.processRawEventAsync(message)
        }
    }
}

internal class DeviceObserver(
    private val onTabsReceived: (Device?, List<TabData>) -> Unit
) : DeviceEventsObserver {
    private val logger = Logger("DeviceObserver")

    override fun onEvents(events: List<DeviceEvent>) {
        events.asSequence()
            .filterIsInstance<DeviceEvent.TabReceived>()
            .forEach { event ->
                logger.debug("Showing ${event.entries.size} tab(s) received from deviceID=${event.from?.id}")

                onTabsReceived(event.from, event.entries)
            }
    }
}

internal class AccountObserver(
    private val feature: AutoPushFeature?
) : SyncAccountObserver {
    private val logger = Logger("AccountObserver")

    override fun onAuthenticated(account: OAuthAccount, newAccount: Boolean) {
        // We need a new subscription only when we have a new account.
        // This is removed when an account logs out.
        if (newAccount) {
            logger.debug("Subscribing for ${PushType.Services} events.")

            feature?.subscribeForType(PushType.Services)
        }
    }

    override fun onLoggedOut() {
        logger.debug("Unsubscribing for ${PushType.Services} events.")

        feature?.unsubscribeForType(PushType.Services)
    }
}

internal inline fun FxaAccountManager.withConstellation(block: (DeviceConstellation) -> Unit) {
    authenticatedAccount()?.let {
        block(it.deviceConstellation())
    }
}
+72 −0
Original line number Diff line number Diff line
/*
 * 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 mozilla.components.feature.sendtab

import mozilla.components.feature.push.AutoPushFeature
import mozilla.components.feature.push.PushType
import mozilla.components.support.test.mock
import org.junit.Test
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions

class AccountObserverTest {
    @Test
    fun `feature and service invoked on new account authenticated`() {
        val feature: AutoPushFeature = mock()
        val observer = AccountObserver(feature)

        observer.onAuthenticated(mock(), true)

        verify(feature).subscribeForType(PushType.Services)

        verifyNoMoreInteractions(feature)
    }

    @Test
    fun `feature and service are not invoked if not provided`() {
        val feature: AutoPushFeature = mock()
        val observer = AccountObserver(null)

        observer.onAuthenticated(mock(), true)
        observer.onLoggedOut()

        verifyNoMoreInteractions(feature)
    }

    @Test
    fun `feature does not subscribe if not a new account`() {
        val feature: AutoPushFeature = mock()
        val observer = AccountObserver(feature)

        observer.onAuthenticated(mock(), false)

        verifyNoMoreInteractions(feature)
    }

    @Test
    fun `feature and service invoked on logout`() {
        val feature: AutoPushFeature = mock()
        val observer = AccountObserver(feature)

        observer.onLoggedOut()

        verify(feature).unsubscribeForType(PushType.Services)

        verifyNoMoreInteractions(feature)
    }

    @Test
    fun `feature and service not invoked for any other callback`() {
        val feature: AutoPushFeature = mock()
        val observer = AccountObserver(feature)

        observer.onAuthenticationProblems()
        observer.onProfileUpdated(mock())

        verifyNoMoreInteractions(feature)
    }
}
 No newline at end of file
+49 −0
Original line number Diff line number Diff line
/*
 * 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 mozilla.components.feature.sendtab

import mozilla.components.concept.sync.Device
import mozilla.components.concept.sync.DeviceEvent
import mozilla.components.concept.sync.TabData
import mozilla.components.support.test.any
import mozilla.components.support.test.eq
import mozilla.components.support.test.mock
import org.junit.Test
import org.mockito.Mockito.times
import org.mockito.Mockito.verify

class DeviceObserverTest {
    @Test
    fun `events are delivered successfully`() {
        val callback: (Device?, List<TabData>) -> Unit = mock()
        val observer = DeviceObserver(callback)
        val events = listOf(DeviceEvent.TabReceived(mock(), mock()))

        observer.onEvents(events)

        verify(callback).invoke(any(), any())

        observer.onEvents(listOf(DeviceEvent.TabReceived(null, mock())))

        verify(callback).invoke(eq(null), any())
    }

    @Test
    fun `only TabReceived events are delivered`() {
        // we don't have other event types right now so this is a basic test.
        val callback: (Device?, List<TabData>) -> Unit = mock()
        val observer = DeviceObserver(callback)
        val events = listOf(
            DeviceEvent.TabReceived(mock(), mock()),
            DeviceEvent.TabReceived(mock(), mock())
        )

        observer.onEvents(events)

        verify(callback, times(2)).invoke(any(), any())
    }
}
 No newline at end of file
Loading