Commit fd5d8ffd authored by MozLando's avatar MozLando
Browse files

Merge #4939



4939:  Closes #4629: Add Gecko WebPush implementation  r=rocketsroger a=jonalmeida



Co-authored-by: default avatarJonathan Almeida <jalmeida@mozilla.com>
parents 33eecbea 35d2de5c
Loading
Loading
Loading
Loading
+21 −0
Original line number Diff line number Diff line
@@ -11,6 +11,8 @@ import mozilla.components.browser.engine.gecko.mediaquery.from
import mozilla.components.browser.engine.gecko.mediaquery.toGeckoValue
import mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
import mozilla.components.browser.engine.gecko.webnotifications.GeckoWebNotificationDelegate
import mozilla.components.browser.engine.gecko.webpush.GeckoWebPushDelegate
import mozilla.components.browser.engine.gecko.webpush.GeckoWebPushHandler
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
@@ -27,6 +29,8 @@ import mozilla.components.concept.engine.utils.EngineVersion
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionDelegate
import mozilla.components.concept.engine.webnotifications.WebNotificationDelegate
import mozilla.components.concept.engine.webpush.WebPushDelegate
import mozilla.components.concept.engine.webpush.WebPushHandler
import org.json.JSONObject
import org.mozilla.geckoview.ContentBlocking
import org.mozilla.geckoview.ContentBlockingController
@@ -42,6 +46,7 @@ import java.lang.IllegalStateException
/**
 * Gecko-based implementation of Engine interface.
 */
@Suppress("TooManyFunctions")
class GeckoEngine(
    context: Context,
    private val defaultSettings: Settings? = null,
@@ -53,6 +58,7 @@ class GeckoEngine(
    private val executor by lazy { executorProvider.invoke() }
    private val localeUpdater = LocaleSettingUpdater(context, runtime)
    private var webExtensionDelegate: WebExtensionDelegate? = null
    private var webPushHandler: WebPushHandler? = null

    init {
        runtime.delegate = GeckoRuntime.Delegate {
@@ -184,6 +190,21 @@ class GeckoEngine(
        runtime.webNotificationDelegate = GeckoWebNotificationDelegate(webNotificationDelegate)
    }

    /**
     * See [Engine.registerWebPushDelegate].
     */
    override fun registerWebPushDelegate(
        webPushDelegate: WebPushDelegate
    ): WebPushHandler {
        runtime.webPushController.setDelegate(GeckoWebPushDelegate(webPushDelegate))

        if (webPushHandler == null) {
            webPushHandler = GeckoWebPushHandler(runtime)
        }

        return requireNotNull(webPushHandler)
    }

    /**
     * See [Engine.clearData].
     */
+71 −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.browser.engine.gecko.webpush

import mozilla.components.concept.engine.webpush.WebPushDelegate
import mozilla.components.concept.engine.webpush.WebPushSubscription
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.WebPushDelegate as GeckoViewWebPushDelegate
import org.mozilla.geckoview.WebPushSubscription as GeckoWebPushSubscription

/**
 * A wrapper for the [WebPushDelegate] to communicate with the Gecko-based delegate.
 */
internal class GeckoWebPushDelegate(private val delegate: WebPushDelegate) : GeckoViewWebPushDelegate {

    /**
     * See [GeckoViewWebPushDelegate.onGetSubscription].
     */
    override fun onGetSubscription(scope: String): GeckoResult<GeckoWebPushSubscription>? {
        val result: GeckoResult<GeckoWebPushSubscription> = GeckoResult()

        delegate.onGetSubscription(scope) { subscription ->
            result.complete(subscription.toGeckoSubscription())
        }

        return result
    }

    /**
     * See [GeckoViewWebPushDelegate.onSubscribe].
     */
    override fun onSubscribe(scope: String, appServerKey: ByteArray?): GeckoResult<GeckoWebPushSubscription>? {
        val result: GeckoResult<GeckoWebPushSubscription> = GeckoResult()

        delegate.onSubscribe(scope, appServerKey) { subscription ->
            result.complete(subscription.toGeckoSubscription())
        }

        return result
    }

    /**
     * See [GeckoViewWebPushDelegate.onUnsubscribe].
     */
    override fun onUnsubscribe(scope: String): GeckoResult<Void>? {
        val result: GeckoResult<Void> = GeckoResult()

        delegate.onUnsubscribe(scope) { success ->
            if (success) {
                result.complete(null)
            } else {
                result.completeExceptionally(IllegalStateException("Un-subscribing from subscription failed."))
            }
        }

        return result
    }
}

/**
 * A helper extension to convert the subscription data class to the Gecko-based implementation.
 */
internal fun WebPushSubscription.toGeckoSubscription() = GeckoWebPushSubscription(
    scope,
    endpoint,
    appServerKey,
    publicKey,
    authSecret
)
+32 −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.browser.engine.gecko.webpush

import mozilla.components.concept.engine.webpush.WebPushHandler
import mozilla.components.concept.engine.webpush.WebPushSubscription
import org.mozilla.geckoview.GeckoRuntime

/**
 * Gecko-based implementation of [WebPushHandler], wrapping the
 * controller object provided by GeckoView.
 */
internal class GeckoWebPushHandler(
    private val runtime: GeckoRuntime
) : WebPushHandler {

    /**
     * See [WebPushHandler].
     */
    override fun onPushMessage(subscription: WebPushSubscription, message: ByteArray?) {
        runtime.webPushController.onPushEvent(subscription.toGeckoSubscription(), message)
    }

    /**
     * See [WebPushHandler].
     */
    override fun onSubscriptionChanged(subscription: WebPushSubscription) {
        runtime.webPushController.onSubscriptionChanged(subscription.toGeckoSubscription())
    }
}
+27 −0
Original line number Diff line number Diff line
@@ -50,6 +50,7 @@ import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoWebExecutor
import org.mozilla.geckoview.StorageController
import org.mozilla.geckoview.WebExtensionController
import org.mozilla.geckoview.WebPushController
import org.robolectric.Robolectric
import java.io.IOException
import java.lang.Exception
@@ -699,6 +700,32 @@ class GeckoEngineTest {
        assertTrue(onErrorCalled)
    }

    @Test
    fun `registerWebNotificationDelegate sets delegate`() {
        val runtime = mock<GeckoRuntime>()
        val engine = GeckoEngine(context, runtime = runtime)

        engine.registerWebNotificationDelegate(mock())

        verify(runtime).webNotificationDelegate = any()
    }

    @Test
    fun `registerWebPushDelegate sets delegate and returns same handler`() {
        val runtime = mock<GeckoRuntime>()
        val controller: WebPushController = mock()
        val engine = GeckoEngine(context, runtime = runtime)

        whenever(runtime.webPushController).thenReturn(controller)

        val handler1 = engine.registerWebPushDelegate(mock())
        val handler2 = engine.registerWebPushDelegate(mock())

        verify(controller, times(2)).setDelegate(any())

        assert(handler1 == handler2)
    }

    private fun createDummyLogEntryList(): List<ContentBlockingController.LogEntry> {
        val addLogEntry = object : ContentBlockingController.LogEntry() {}

+138 −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.browser.engine.gecko.webpush

import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.concept.engine.webpush.WebPushDelegate
import mozilla.components.concept.engine.webpush.WebPushSubscription
import mozilla.components.support.test.any
import mozilla.components.support.test.eq
import mozilla.components.support.test.mock
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.isNull
import org.mockito.Mockito.verify
import org.mozilla.geckoview.GeckoResult

@RunWith(AndroidJUnit4::class)
class GeckoWebPushDelegateTest {

    @Test
    fun `delegate is always invoked`() {
        val delegate: WebPushDelegate = mock()
        val geckoDelegate = GeckoWebPushDelegate(delegate)

        geckoDelegate.onGetSubscription("test")

        verify(delegate).onGetSubscription(eq("test"), any())

        geckoDelegate.onSubscribe("test", null)

        verify(delegate).onSubscribe(eq("test"), isNull(), any())

        geckoDelegate.onSubscribe("test", "key".toByteArray())

        verify(delegate).onSubscribe(eq("test"), eq("key".toByteArray()), any())

        geckoDelegate.onUnsubscribe("test")

        verify(delegate).onUnsubscribe(eq("test"), any())
    }

    @Test
    fun `onGetSubscription result is completed`() {
        val subscription = WebPushSubscription(
            "test",
            "https://example.com",
            null,
            ByteArray(65),
            ByteArray(16)
        )
        val delegate: WebPushDelegate = object : WebPushDelegate {
            override fun onGetSubscription(
                scope: String,
                onSubscription: (WebPushSubscription) -> Unit
            ) {
                onSubscription(subscription)
            }
        }

        val geckoDelegate = GeckoWebPushDelegate(delegate)
        val result = geckoDelegate.onGetSubscription("test")

        result?.accept { sub ->
            assert(sub!!.scope == subscription.scope)
        }
    }

    @Test
    fun `onSubscribe result is completed`() {
        val subscription = WebPushSubscription(
            "test",
            "https://example.com",
            null,
            ByteArray(65),
            ByteArray(16)
        )
        val delegate: WebPushDelegate = object : WebPushDelegate {
            override fun onSubscribe(
                scope: String,
                serverKey: ByteArray?,
                onSubscribe: (WebPushSubscription) -> Unit
            ) {
                onSubscribe(subscription)
            }
        }

        val geckoDelegate = GeckoWebPushDelegate(delegate)
        val result = geckoDelegate.onSubscribe("test", null)

        result?.accept { sub ->
            assert(sub!!.scope == subscription.scope)
            assertNull(sub.appServerKey)
        }
    }

    @Test
    fun `onUnsubscribe result is completed successfully`() {
        val delegate: WebPushDelegate = object : WebPushDelegate {
            override fun onUnsubscribe(
                scope: String,
                onUnsubscribe: (Boolean) -> Unit
            ) {
                onUnsubscribe(true)
            }
        }

        val geckoDelegate = GeckoWebPushDelegate(delegate)
        val result = geckoDelegate.onUnsubscribe("test")

        result?.accept { sub ->
            assertNull(sub)
        }
    }

    @Test
    fun `onUnsubscribe result receives throwable when unsuccessful`() {
        val delegate: WebPushDelegate = object : WebPushDelegate {
            override fun onUnsubscribe(
                scope: String,
                onUnsubscribe: (Boolean) -> Unit
            ) {
                onUnsubscribe(false)
            }
        }

        val geckoDelegate = GeckoWebPushDelegate(delegate)

        val result = geckoDelegate.onUnsubscribe("test")

        result?.exceptionally<Void> { ex ->
            assert(ex.toString().contains("Un-subscribing from subscription failed"))
            GeckoResult.fromValue(null)
        }
    }
}
Loading