Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Close #9829: Move IO work in FxaPushSupportFeature to a coroutine #12687

Merged
merged 1 commit into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion components/feature/accounts-push/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ in order to fix the problem. Therefore, if FxA and Push are used together,
include the support feature as well:

```kotlin
FxaPushSupportFeature(context, fxaAccountManager, autoPushFeature)
val feature = FxaPushSupportFeature(context, fxaAccountManager, autoPushFeature)

// initialize the feature; this can be done immediately if needed.
feature.initalize()
```

## License
Expand Down
6 changes: 6 additions & 0 deletions components/feature/accounts-push/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ android {
}
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
}
}

dependencies {
implementation project(':service-firefox-accounts')
implementation project(':support-ktx')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.lifecycle.ProcessLifecycleOwner
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import mozilla.components.concept.base.crash.Breadcrumb
Expand All @@ -24,6 +25,7 @@ import mozilla.components.concept.sync.DeviceConstellation
import mozilla.components.concept.sync.DeviceConstellationObserver
import mozilla.components.concept.sync.DevicePushSubscription
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.feature.accounts.push.cache.PushScopeProperty
import mozilla.components.feature.push.AutoPushFeature
import mozilla.components.feature.push.AutoPushSubscription
import mozilla.components.feature.push.PushScope
Expand All @@ -32,7 +34,6 @@ import mozilla.components.service.fxa.manager.ext.withConstellation
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.base.utils.SharedPreferencesCache
import org.json.JSONObject
import java.util.UUID
import mozilla.components.concept.sync.AccountObserver as SyncAccountObserver

internal const val PREFERENCE_NAME = "mozac_feature_accounts_push"
Expand All @@ -48,61 +49,51 @@ internal const val PREF_FXA_SCOPE = "fxa_push_scope"
* @param accountManager The FxaAccountManager.
* @param pushFeature The [AutoPushFeature] if that is setup for observing push events.
* @param crashReporter Instance of `CrashReporting` to record unexpected caught exceptions.
* @param coroutineScope The scope in which IO work within the feature should be performed on.
* @param owner the lifecycle owner for the observer. Defaults to [ProcessLifecycleOwner].
* @param autoPause whether to stop notifying the observer during onPause lifecycle events.
* Defaults to false so that observers are always notified.
*/
class FxaPushSupportFeature(
private val context: Context,
accountManager: FxaAccountManager,
pushFeature: AutoPushFeature,
private val accountManager: FxaAccountManager,
private val pushFeature: AutoPushFeature,
private val crashReporter: CrashReporting? = null,
owner: LifecycleOwner = ProcessLifecycleOwner.get(),
autoPause: Boolean = false
private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO),
private val owner: LifecycleOwner = ProcessLifecycleOwner.get(),
private val autoPause: Boolean = false
) {

/**
* A unique scope for the FxA push subscription that is generated once and stored in SharedPreferences.
*
* This scope is randomly generated and unique to the account on that particular device.
* This scope is randomly generated and unique to the app install.
* (why this uuid? Note it is *not* reset on logout!)
* (isn't this racey? Different observers reference it)
* (maybe you just want the acct uid?)
*/
private val fxaPushScope: String by lazy {
val prefs = preference(context)

// Generate a unique scope if one doesn't exist.
val randomUuid = UUID.randomUUID().toString().replace("-", "")

// Return a scope in the format example: "fxa_push_scope_a62d5f27c9d74af4996d057f0e0e9c38"
val scope = PUSH_SCOPE_PREFIX + randomUuid

if (!prefs.contains(PREF_FXA_SCOPE)) {
prefs.edit().putString(PREF_FXA_SCOPE, scope).apply()

return@lazy scope
}
private val pushScope = PushScopeProperty(context, coroutineScope)

// The default string is non-null, so we can safely cast.
prefs.getString(PREF_FXA_SCOPE, scope) as String
}
/**
* Initialize the support feature to launch the appropriate observers.
*/
fun initialize() = coroutineScope.launch {
val scopeValue = pushScope.value()

init {
val autoPushObserver = AutoPushObserver(accountManager, pushFeature, fxaPushScope)
val autoPushObserver = AutoPushObserver(accountManager, pushFeature, scopeValue)

val accountObserver = AccountObserver(
context,
pushFeature,
fxaPushScope,
scopeValue,
crashReporter,
owner,
autoPause
)

accountManager.register(accountObserver)
coroutineScope.launch(Main) {
accountManager.register(accountObserver)

pushFeature.register(autoPushObserver, owner, autoPause)
pushFeature.register(autoPushObserver, owner, autoPause)
}
}

companion object {
Expand All @@ -128,7 +119,6 @@ internal class AccountObserver(

@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {

val constellationObserver = ConstellationObserver(
context = context,
push = push,
Expand All @@ -142,7 +132,7 @@ internal class AccountObserver(
// registration could happen after onDevicesUpdate has been called, without having to tie this
// into the account "auth lifecycle".
// See https://github.com/mozilla-mobile/android-components/issues/8766
GlobalScope.launch(Dispatchers.Main) {
GlobalScope.launch(Main) {
account.deviceConstellation().registerDeviceObserver(constellationObserver, lifecycleOwner, autoPause)
account.deviceConstellation().refreshDevices()
}
Expand Down Expand Up @@ -195,7 +185,7 @@ internal fun pushSubscribe(
currentDevice.subscription?.endpoint != subscription.endpoint
) {
logger.info("Updating account with new subscription info.")
CoroutineScope(Dispatchers.Main).launch {
CoroutineScope(Main).launch {
account.deviceConstellation().setDevicePushSubscription(subscription.into())
}
}
Expand Down Expand Up @@ -271,7 +261,7 @@ internal class AutoPushObserver(
val rawEvent = message ?: return

accountManager.withConstellation {
CoroutineScope(Dispatchers.Main).launch {
CoroutineScope(Main).launch {
processRawEvent(String(rawEvent))
}
}
Expand Down Expand Up @@ -395,7 +385,6 @@ internal class VerificationDelegate(

internal data class VerificationState(val timestamp: Long, val totalCount: Int)

@VisibleForTesting
internal fun preference(context: Context) = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)

internal fun AutoPushSubscription.into() = DevicePushSubscription(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* 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.accounts.push.cache

import android.content.Context
import android.content.SharedPreferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext
import mozilla.components.feature.accounts.push.FxaPushSupportFeature
import mozilla.components.feature.accounts.push.PREF_FXA_SCOPE
import mozilla.components.feature.accounts.push.preference
import mozilla.components.feature.push.PushScope
import java.util.UUID

/**
* An implementation of a [ScopeProperty] that generates and stores a scope in [SharedPreferences].
*/
internal class PushScopeProperty(
private val context: Context,
private val coroutineScope: CoroutineScope,
) : ScopeProperty {

override suspend fun value(): PushScope = withContext(coroutineScope.coroutineContext) {
val prefs = preference(context)

// Generate a unique scope if one doesn't exist.
val randomUuid = UUID.randomUUID().toString().replace("-", "")

// Return a scope in the format example: "fxa_push_scope_a62d5f27c9d74af4996d057f0e0e9c38"
val scope = FxaPushSupportFeature.PUSH_SCOPE_PREFIX + randomUuid

if (!prefs.contains(PREF_FXA_SCOPE)) {
prefs.edit().putString(PREF_FXA_SCOPE, scope).apply()

return@withContext scope
}

// The default string is non-null, so we can safely cast.
prefs.getString(PREF_FXA_SCOPE, scope) as String
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* 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.accounts.push.cache

import mozilla.components.feature.push.PushScope

/**
* A [ScopeProperty] implementation generates and holds the [PushScope].
*/
interface ScopeProperty {

/**
* Returns the [PushScope] value.
*/
suspend fun value(): PushScope
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
Expand All @@ -25,26 +27,39 @@ import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class FxaPushSupportFeatureTest {

@get:Rule
val coroutineTestRule = MainCoroutineRule()

private val pushFeature: AutoPushFeature = mock()
private val accountManager: FxaAccountManager = mock()

private lateinit var feature: FxaPushSupportFeature

@Before
fun setup() {
preference(testContext).edit().remove(PREF_FXA_SCOPE).apply()
`when`(pushFeature.config).thenReturn(mock())

feature = FxaPushSupportFeature(
context = testContext,
accountManager = accountManager,
pushFeature = pushFeature,
crashReporter = null,
coroutineScope = coroutineTestRule.scope
)
}

@Test
fun `account observer registered`() {
FxaPushSupportFeature(testContext, accountManager, pushFeature)
feature.initialize()

verify(accountManager).register(any())
verify(pushFeature).register(any(), any(), anyBoolean())
}

@Test
fun `feature generates and caches a scope`() {
FxaPushSupportFeature(testContext, accountManager, pushFeature)
feature.initialize()

assertTrue(preference(testContext).contains(PREF_FXA_SCOPE))
}
Expand All @@ -53,15 +68,15 @@ class FxaPushSupportFeatureTest {
fun `feature does not generate a scope if one already exists`() {
preference(testContext).edit().putString(PREF_FXA_SCOPE, "testScope").apply()

FxaPushSupportFeature(testContext, accountManager, pushFeature)
feature.initialize()

val cachedScope = preference(testContext).getString(PREF_FXA_SCOPE, "")
assertEquals("testScope", cachedScope!!)
}

@Test
fun `feature generates a partially predictable push scope`() {
FxaPushSupportFeature(testContext, accountManager, pushFeature)
feature.initialize()

val cachedScope = preference(testContext).getString(PREF_FXA_SCOPE, "")

Expand Down
6 changes: 5 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ permalink: /changelog/
* **support-ktx**
* 🚒 Bug fixed [issue #12689](https://github.com/mozilla-mobile/android-components/issues/12689) Make `Context.shareMedia` work with Android Direct Share.

* **feature-accounts-push**:
* ⚠️ **This is a breaking change**: `FxaPushSupportFeature` now requires to be explicitly started with `initialize`.
* The constructor for `FxaPushSupportFeature` has a `coroutineScope` parameter that defaults to a `CoroutineScope(Dispatchers.IO)`.

# 105.0.0
* [Commits](https://github.com/mozilla-mobile/android-components/compare/v104.0.0...v105.0.0)
* [Milestone](https://github.com/mozilla-mobile/android-components/milestone/152?closed=1)
Expand All @@ -29,7 +33,7 @@ permalink: /changelog/

* **feature-findinpage**:
* 🚒 Bug fixed [issue #12637](https://github.com/mozilla-mobile/android-components/issues/12637) Disable find in page previous and forward buttons if the query is empty

* **feature-search**:
* Implement the common part of search widget in Android Components [#12565](https://github.com/mozilla-mobile/android-components/issues/12565).

Expand Down