[fenix] Closes https://github.com/mozilla-mobile/fenix/issues/6730: Lazily initialize account manager on new push message
parent
93f2c58d76
commit
acc3bb4ec6
@ -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() }
|
||||
}
|
@ -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()
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue