1
0
Fork 0

Closes #6730: Lazily initialize account manager on new push message

master
Jonathan Almeida 2020-02-28 17:09:06 -05:00 committed by Jonathan Almeida
parent d390baefaf
commit 4bbf87d272
7 changed files with 240 additions and 41 deletions

View File

@ -36,6 +36,7 @@ import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.push.PushFxaIntegration
import org.mozilla.fenix.session.NotificationSessionObserver
import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
import org.mozilla.fenix.session.VisibilityLifecycleCallback
@ -175,14 +176,17 @@ open class FenixApplication : LocaleAwareApplication() {
// Sets the PushFeature as the singleton instance for push messages to go to.
// We need the push feature setup here to deliver messages in the case where the service
// starts up the app first.
components.backgroundServices.push?.let { autoPushFeature ->
components.push.feature?.let {
Logger.info("AutoPushFeature is configured, initializing it...")
// Install the AutoPush singleton to receive messages.
PushProcessor.install(autoPushFeature)
PushProcessor.install(it)
// Perform a one-time initialization of the account manager if a message is received.
PushFxaIntegration(it, lazy { components.backgroundServices.accountManager }).launch()
// Initialize the service. This could potentially be done in a coroutine in the future.
autoPushFeature.initialize()
it.initialize()
}
}

View File

@ -17,8 +17,6 @@ import mozilla.components.concept.sync.DeviceType
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.feature.accounts.push.FxaPushSupportFeature
import mozilla.components.feature.accounts.push.SendTabFeature
import mozilla.components.feature.push.AutoPushFeature
import mozilla.components.feature.push.PushConfig
import mozilla.components.lib.crash.CrashReporter
import mozilla.components.service.fxa.DeviceConfig
import mozilla.components.service.fxa.ServerConfig
@ -30,7 +28,6 @@ import mozilla.components.service.fxa.manager.SCOPE_SYNC
import mozilla.components.service.fxa.manager.SyncEnginesStorage
import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider
import mozilla.components.service.sync.logins.SyncableLoginsStorage
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
@ -47,6 +44,7 @@ import org.mozilla.fenix.test.Mockable
@Mockable
class BackgroundServices(
private val context: Context,
private val push: Push,
crashReporter: CrashReporter,
historyStorage: PlacesHistoryStorage,
bookmarkStorage: PlacesBookmarksStorage,
@ -86,10 +84,6 @@ class BackgroundServices(
syncPeriodInMinutes = 240L) // four hours
}
private val pushService by lazy { FirebasePushService() }
val push by lazy { makePushConfig()?.let { makePush(it) } }
init {
// Make the "history", "bookmark", and "passwords" stores accessible to workers spawned by the sync manager.
GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage)
@ -106,30 +100,6 @@ class BackgroundServices(
val accountManager = makeAccountManager(context, serverConfig, deviceConfig, syncConfig)
@VisibleForTesting(otherwise = PRIVATE)
fun makePush(pushConfig: PushConfig): AutoPushFeature {
return AutoPushFeature(
context = context,
service = pushService,
config = pushConfig
)
}
@VisibleForTesting(otherwise = PRIVATE)
fun makePushConfig(): PushConfig? {
val logger = Logger("PushConfig")
val projectIdKey = context.getString(R.string.pref_key_push_project_id)
val resId = context.resources.getIdentifier(projectIdKey, "string", context.packageName)
if (resId == 0) {
logger.warn("No firebase configuration found; cannot support push service.")
return null
}
logger.debug("Creating push configuration for autopush.")
val projectId = context.resources.getString(resId)
return PushConfig(projectId)
}
@VisibleForTesting(otherwise = PRIVATE)
fun makeAccountManager(
context: Context,
@ -166,7 +136,7 @@ class BackgroundServices(
accountManager.register(accountAbnormalities)
// Enable push if it's configured.
push?.let { autoPushFeature ->
push.feature?.let { autoPushFeature ->
FxaPushSupportFeature(context, accountManager, autoPushFeature)
}

View File

@ -26,6 +26,7 @@ class Components(private val context: Context) {
val backgroundServices by lazy {
BackgroundServices(
context,
push,
analytics.crashReporter,
core.historyStorage,
core.bookmarksStorage,
@ -78,4 +79,5 @@ class Components(private val context: Context) {
val clipboardHandler by lazy { ClipboardHandler(context) }
val migrationStore by lazy { MigrationStore() }
val performance by lazy { PerformanceComponent() }
val push by lazy { Push(context) }
}

View File

@ -0,0 +1,39 @@
package org.mozilla.fenix.components
import android.content.Context
import mozilla.components.feature.push.AutoPushFeature
import mozilla.components.feature.push.PushConfig
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.R
/**
* Component group for push services. These components use services that strongly depend on
* push messaging (e.g. WebPush, SendTab).
*/
class Push(context: Context) {
val feature by lazy {
pushConfig?.let { config ->
AutoPushFeature(
context = context,
service = pushService,
config = config
)
}
}
private val pushConfig: PushConfig? by lazy {
val logger = Logger("PushConfig")
val projectIdKey = context.getString(R.string.pref_key_push_project_id)
val resId = context.resources.getIdentifier(projectIdKey, "string", context.packageName)
if (resId == 0) {
logger.warn("No firebase configuration found; cannot support push service.")
return@lazy null
}
logger.debug("Creating push configuration for autopush.")
val projectId = context.resources.getString(resId)
PushConfig(projectId)
}
private val pushService by lazy { FirebasePushService() }
}

View File

@ -0,0 +1,62 @@
/* 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.push
import android.annotation.SuppressLint
import com.google.firebase.messaging.RemoteMessage
import com.google.firebase.messaging.FirebaseMessagingService
import com.leanplum.LeanplumPushFirebaseMessagingService
import mozilla.components.concept.push.PushService
import mozilla.components.lib.push.firebase.AbstractFirebasePushService
import mozilla.components.feature.push.AutoPushFeature
/**
* A wrapper class that only exists to delegate to [FirebaseMessagingService] instances.
*
* Implementation notes:
*
* This was a doozy...
*
* With Firebase Cloud Messaging, we've been given some tight constraints in order to get this to
* work:
* - We want to have multiple FCM message receivers for AutoPush and LeanPlum (for now), however
* there can only be one registered [FirebaseMessagingService] in the AndroidManifest.
* - The [LeanplumPushFirebaseMessagingService] does not function as expected unless it's the
* inherited service that receives the messages.
* - The [AutoPushService] is not strongly tied to being the inherited service, but the
* [AutoPushFeature] requires a reference to the push instance as a [PushService].
*
* We tried creating an empty [FirebaseMessagingService] that can hold a list of the services
* for delegating, but the [LeanplumPushFirebaseMessagingService] tries to get a reference to the
* Application Context, however,since the FCM service runs in a background process that gives a
* nullptr. Within LeanPlum, this is something that is probably provided internally.
*
* We tried to pass in an instance of the [AbstractFirebasePushService] to [FirebasePushService]
* through the constructor and delegate the implementation of a [PushService] to that, but alas,
* the service requires you to have an empty default constructor in order for the OS to do the
* initialization. For this reason, we created a singleton instance of the AutoPush instance since
* that lets us easily delegate the implementation to that, as well as make invocations when FCM
* receives new messages.
*/
class FirebasePushService : LeanplumPushFirebaseMessagingService(),
PushService by AutoPushService {
override fun onNewToken(newToken: String) {
AutoPushService.onNewToken(newToken)
super.onNewToken(newToken)
}
override fun onMessageReceived(remoteMessage: RemoteMessage?) {
AutoPushService.onMessageReceived(remoteMessage)
super.onMessageReceived(remoteMessage)
}
}
/**
* A singleton instance of the FirebasePushService needed for communicating between FCM and the
* [AutoPushFeature].
*/
@SuppressLint("MissingFirebaseInstanceTokenRefresh") // Implemented internally.
object AutoPushService : AbstractFirebasePushService()

View File

@ -0,0 +1,127 @@
/* 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.push
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.feature.accounts.push.FxaPushSupportFeature
import mozilla.components.feature.accounts.push.SendTabFeature
import mozilla.components.feature.push.AutoPushFeature
import mozilla.components.feature.push.PushScope
import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.service.fxa.manager.ext.withConstellation
import org.mozilla.fenix.components.BackgroundServices
import org.mozilla.fenix.components.Push
/**
* A lazy initializer for FxaAccountManager if it isn't already initialized.
*
* Implementation notes: For push notifications, we need to initialize the service on
* Application#onCreate as soon as possible in order to receive messages. These are then decrypted
* and the observers of the push feature are notified.
*
* One of our observers is [FxaAccountManager] that needs to know about messages like Send Tab,
* new account logins, etc. This however comes at the cost of having the account manager
* initialized and observing the push feature when it initializes (which once again happens on
* application create) - the total cost of startup time now is additive for the both of them.
*
* What this integration class aims to do, is to observe the push feature immediately in order to act
* as a (temporary) delegate, and when we see a push message from FxA, only then we should
* initialize and deliver the message.
*
* Once FxaAccountManager is initialized, we no longer need this integration as there already are
* existing features to support these feature requirements, so we safely unregister ourselves.
* See: [FxaPushSupportFeature] and [SendTabFeature].
*
* A solution that we considered was to pass in [BackgroundServices] to the [Push] class
* and lazily invoke the account manager - that lead to a cyclic dependency of initialization since
* [BackgroundServices] also depends on [Push] directly for observing messages via the account-based
* features.
*
* Another solution was to create a message buffer to queue up the messages until the account could
* consume them - this added the complexity of maintaining a buffer, the possibility of flooding the
* buffer, and delaying the delivery of high importance messages like Send Tab which are required to
* be processed immediately.
*
* Our final solution ended up being more concise that the above options that met all our required
* assurances, and most importantly, maintainable.
*/
class PushFxaIntegration(
private val pushFeature: AutoPushFeature,
lazyAccountManager: Lazy<FxaAccountManager>
) {
private val observer =
OneTimePushMessageObserver(
lazyAccountManager,
pushFeature
)
/**
* Starts the observer.
*
* This should be done before or as soon as push is initialized.
*/
fun launch() {
pushFeature.register(observer)
}
}
/**
* Observes push messages from [AutoPushFeature], then initializes [FxaAccountManager] if it isn't
* already.
*/
internal class OneTimePushMessageObserver(
private val lazyAccountManager: Lazy<FxaAccountManager>,
private val pushFeature: AutoPushFeature
) : AutoPushFeature.Observer {
override fun onMessageReceived(scope: PushScope, message: ByteArray?) {
// Ignore empty push messages.
val rawBytes = message ?: return
// If the push scope has the FxA prefix, we know this is for us.
if (scope.contains(FxaPushSupportFeature.PUSH_SCOPE_PREFIX)) {
// If we aren't initialized, then we should do the initialization and message delivery.
if (!lazyAccountManager.isInitialized()) {
CoroutineScope(Dispatchers.Main).launch {
val fxaObserver = OneTimeMessageDeliveryObserver(lazyAccountManager, rawBytes)
// Start observing the account manager, so that we can deliver our message
// only when we are authenticated and are capable of processing it.
lazyAccountManager.value.register(fxaObserver)
}
}
// Remove ourselves when we're done.
pushFeature.unregister(this)
}
}
}
/**
* Waits for the [FxaAccountManager] to authenticate itself in order to deliver the [message], then
* unregisters itself once complete.
*/
internal class OneTimeMessageDeliveryObserver(
private val lazyAccount: Lazy<FxaAccountManager>,
private val message: ByteArray
) : AccountObserver {
override fun onAuthenticated(
account: OAuthAccount,
authType: AuthType
) {
lazyAccount.value.withConstellation {
it.processRawEventAsync(String(message))
}
lazyAccount.value.unregister(this)
}
}

View File

@ -13,8 +13,6 @@ import io.mockk.verify
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.feature.push.AutoPushFeature
import mozilla.components.feature.push.PushConfig
import mozilla.components.service.fxa.DeviceConfig
import mozilla.components.service.fxa.ServerConfig
import mozilla.components.service.fxa.SyncConfig
@ -27,16 +25,13 @@ import org.mozilla.fenix.components.metrics.MetricController
class BackgroundServicesTest {
class TestableBackgroundServices(
val context: Context
) : BackgroundServices(context, mockk(), mockk(), mockk(), mockk()) {
) : BackgroundServices(context, mockk(), mockk(), mockk(), mockk(), mockk()) {
override fun makeAccountManager(
context: Context,
serverConfig: ServerConfig,
deviceConfig: DeviceConfig,
syncConfig: SyncConfig?
) = mockk<FxaAccountManager>(relaxed = true)
override fun makePushConfig() = mockk<PushConfig>(relaxed = true)
override fun makePush(pushConfig: PushConfig) = mockk<AutoPushFeature>(relaxed = true)
}
@Test