Verified Commit b781a92b authored by Gela's avatar Gela Committed by ma1
Browse files

Bug 2053320 - Part 1: Don't tie Nimbus tooling to `HomeActivity` UI a=RyanVM

parent 23f5704c
Loading
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -820,6 +820,16 @@
            <action android:name="org.mozilla.fenix.TRIGGER_MESSAGE_WORKER" />
          </intent-filter>
        </receiver>

        <receiver
          android:name="org.mozilla.fenix.experiments.QANimbusToolingReceiver"
          android:exported="true"
          android:enabled="true"
          android:permission="android.permission.DUMP">
          <intent-filter>
            <action android:name="org.mozilla.fenix.NIMBUS_TOOLING" />
          </intent-filter>
        </receiver>
    </application>

</manifest>
+0 −4
Original line number Diff line number Diff line
@@ -99,7 +99,6 @@ import mozilla.components.support.utils.toSafeIntent
import mozilla.components.support.webextensions.WebExtensionOptionsPageObserver
import mozilla.components.support.webextensions.WebExtensionPopupObserver
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.experiments.nimbus.initializeTooling
import org.mozilla.fenix.GleanMetrics.AppIcon
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.Metrics
@@ -463,9 +462,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity, Crash
                }
            }
        }

        // Setup nimbus-cli tooling. This is a NOOP when launching normally.
        components.nimbus.sdk.initializeTooling(applicationContext, intent)
        components.strictMode.attachListenerToDisablePenaltyDeath(supportFragmentManager)
        MarkersFragmentLifecycleCallbacks.register(supportFragmentManager, components.core.engine)

+57 −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 org.mozilla.fenix.experiments

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.experiments.nimbus.initializeTooling
import org.mozilla.fenix.ext.components

private val logger = Logger("QANimbusToolingReceiver")

/**
 * Receiver triggered on demand via `nimbus-cli` to manually enroll into Nimbus experiments.
 *
 * ```
 *   adb shell am broadcast -a org.mozilla.fenix.NIMBUS_TOOLING \
 *       -p org.mozilla.fenix
 * ```
 *
 * `-p org.mozilla.fenix` is the package name, so adjust that value for release/beta/nightly/debug.
 *
 * @param dispatcher the [CoroutineDispatcher] the tooling commands are applied on.
 */
class QANimbusToolingReceiver(private val dispatcher: CoroutineDispatcher = Dispatchers.IO) : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action != ACTION_NIMBUS_TOOLING) return

        logger.info("Enqueueing QANimbusToolingReceiver via debug trigger")

        val applicationContext = context.applicationContext

        val pendingResult: PendingResult? = goAsync()
        CoroutineScope(dispatcher).launch {
            try {
                applicationContext.components.nimbus.sdk.initializeTooling(
                    applicationContext,
                    intent,
                )
            } finally {
                logger.info("Nimbus tooling command processed")
                pendingResult?.finish()
            }
        }
    }

    companion object {
        const val ACTION_NIMBUS_TOOLING = "org.mozilla.fenix.NIMBUS_TOOLING"
    }
}
+117 −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 org.mozilla.fenix.experiments

import android.content.Context
import android.content.Intent
import io.mockk.every
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.experiments.QANimbusToolingReceiver.Companion.ACTION_NIMBUS_TOOLING
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.nimbus.TestNimbusApi
import org.robolectric.RobolectricTestRunner

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
class QANimbusToolingReceiverTest {

    private val nimbusApi = FakeNimbusApi(testContext)
    private val receiver = QANimbusToolingReceiver(UnconfinedTestDispatcher())

    @Before
    fun setUp() {
        every { testContext.components.nimbus.sdk } returns nimbusApi
    }

    @Test
    fun `GIVEN a tooling command WHEN the tooling action is received THEN the command is applied`() {
        receiver.onReceive(testContext, toolingIntent(ACTION_NIMBUS_TOOLING))

        assertEquals(EXPERIMENTS, nimbusApi.appliedExperiments)
        assertEquals(false, nimbusApi.fetchEnabled)
        assertTrue(nimbusApi.databaseReset)
        assertTrue(nimbusApi.stateDumped)
    }

    @Test
    fun `GIVEN a tooling command WHEN another action is received THEN the command is ignored`() {
        receiver.onReceive(testContext, toolingIntent("org.mozilla.fenix.ACTION_PRINT"))

        assertNull(nimbusApi.appliedExperiments)
        assertNull(nimbusApi.fetchEnabled)
        assertFalse(nimbusApi.databaseReset)
        assertFalse(nimbusApi.stateDumped)
    }

    @Test
    fun `GIVEN no tooling command WHEN the tooling action is received THEN nothing is applied`() {
        receiver.onReceive(testContext, Intent(ACTION_NIMBUS_TOOLING))

        assertNull(nimbusApi.appliedExperiments)
        assertNull(nimbusApi.fetchEnabled)
        assertFalse(nimbusApi.databaseReset)
        assertFalse(nimbusApi.stateDumped)
    }

    @Test
    fun `GIVEN a tooling command without the version extra WHEN the tooling action is received THEN nothing is applied`() {
        val intent = toolingIntent(ACTION_NIMBUS_TOOLING).apply { removeExtra("version") }

        receiver.onReceive(testContext, intent)

        assertNull(nimbusApi.appliedExperiments)
        assertFalse(nimbusApi.stateDumped)
    }

    private fun toolingIntent(action: String) =
        Intent(action).apply {
            putExtra("nimbus-cli", null as String?)
            putExtra("version", 1)
            putExtra("experiments", EXPERIMENTS)
            putExtra("reset-db", true)
            putExtra("log-state", true)
        }

    private class FakeNimbusApi(context: Context) : TestNimbusApi(context) {
        var appliedExperiments: String? = null
        var fetchEnabled: Boolean? = null
        var databaseReset = false
        var stateDumped = false

        override fun applyLocalExperiments(experimentsJson: String): Job {
            appliedExperiments = experimentsJson
            return completedJob()
        }

        override fun resetEnrollmentsDatabase(): Job {
            databaseReset = true
            return completedJob()
        }

        override fun setFetchEnabled(enabled: Boolean) {
            fetchEnabled = enabled
        }

        override fun dumpStateToLog() {
            stateDumped = true
        }

        private fun completedJob() = Job().apply { complete() }
    }

    companion object {
        private const val EXPERIMENTS = """{"data":[]}"""
    }
}