From 1877300abf59ed81d41eefc91fd2bfc3bf4415ab Mon Sep 17 00:00:00 2001 From: celenity Date: Wed, 2 Jul 2025 19:25:47 -0400 Subject: [PATCH 1/5] feat: Add support for UnifiedPush Signed-off-by: celenity --- .../src/main/res/values/ironfox_strings.xml | 8 +- patches/unifiedpush.patch | 2749 +++++++++++++++++ scripts/patches.yaml | 7 + 3 files changed, 2763 insertions(+), 1 deletion(-) create mode 100644 patches/unifiedpush.patch diff --git a/patches/fenix-overlay/app/src/main/res/values/ironfox_strings.xml b/patches/fenix-overlay/app/src/main/res/values/ironfox_strings.xml index d4aff51..b291168 100644 --- a/patches/fenix-overlay/app/src/main/res/values/ironfox_strings.xml +++ b/patches/fenix-overlay/app/src/main/res/values/ironfox_strings.xml @@ -13,4 +13,10 @@ Enable JavaScript Enable JavaScript Just-in-time Compilation (JIT) Enable WebAssembly (WASM) - \ No newline at end of file + + Enable UnifiedPush + + Use UnifiedPush + + UnifiedPush setting modified. Quitting the application to apply changes… + diff --git a/patches/unifiedpush.patch b/patches/unifiedpush.patch new file mode 100644 index 0000000..44122c7 --- /dev/null +++ b/patches/unifiedpush.patch @@ -0,0 +1,2749 @@ +diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml +index f012e43dc0..fc3b947485 100644 +--- a/gradle/libs.versions.toml ++++ b/gradle/libs.versions.toml +@@ -107,6 +107,8 @@ sentry = "8.9.0" + commons-exec = "1.3" + tomlj = "1.1.0" + ++unifiedpush = "3.0.7" ++ + [libraries] + + kotlin-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } +@@ -230,6 +232,7 @@ thirdparty-sentry = { group = "io.sentry", name = "sentry-android", version.ref + thirdparty-zxing = { group = "com.google.zxing", name = "core", version.ref = "zxing" } + thirdparty-disklrucache = { group = "com.jakewharton", name = "disklrucache", version.ref = "disklrucache" } + thirdparty-androidsvg = { group = "com.caverock", name = "androidsvg-aar", version.ref = "androidsvg" } ++thirdparty-unifiedpush = { group = "org.unifiedpush.android", name = "connector", version.ref = "unifiedpush" } + + desugar-jdk-libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugar-jdk-libs" } + firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging", version.ref = "firebase-messaging" } +diff --git a/mobile/android/android-components/.buildconfig.yml b/mobile/android/android-components/.buildconfig.yml +index 26e76a18d2..1dcd401764 100644 +--- a/mobile/android/android-components/.buildconfig.yml ++++ b/mobile/android/android-components/.buildconfig.yml +@@ -594,6 +594,7 @@ projects: + - support-utils + - tooling-lint + - ui-icons ++ - feature-unifiedpush + feature-addons: + description: A feature that provides for managing add-ons. + path: components/feature/addons +@@ -2409,3 +2410,13 @@ projects: + - tooling-lint + - ui-colors + - ui-icons ++ feature-unifiedpush: ++ description: Feature that implements push notifications with UnifiedPush. ++ path: components/feature/unifiedpush ++ publish: true ++ upstream_dependencies: ++ - concept-base ++ - concept-push ++ - support-base ++ - support-test ++ - tooling-lint +diff --git a/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushObserver.kt b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushObserver.kt +new file mode 100644 +index 000000000000..85e18ea0d9f0 +--- /dev/null ++++ b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushObserver.kt +@@ -0,0 +1,21 @@ ++/* 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.concept.push ++ ++/** ++ * Observers that want to receive updates for new subscriptions and messages. ++ */ ++interface PushObserver { ++ ++ /** ++ * A subscription for the scope is available. ++ */ ++ fun onSubscriptionChanged(scope: PushScope) = Unit ++ ++ /** ++ * A messaged has been received for the [scope]. ++ */ ++ fun onMessageReceived(scope: PushScope, message: ByteArray?) = Unit ++} +diff --git a/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushProcessor.kt b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushProcessor.kt +index 909ee23737..6c5d38b8f1 100644 +--- a/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushProcessor.kt ++++ b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushProcessor.kt +@@ -15,12 +15,12 @@ interface PushProcessor { + /** + * Start the push processor and any service associated. + */ +- fun initialize() ++// fun initialize() + + /** + * Removes all push subscriptions from the device. + */ +- fun shutdown() ++// fun shutdown() + + /** + * A new registration token has been received. +@@ -43,7 +43,7 @@ interface PushProcessor { + /** + * Requests the [PushService] to renew it's registration with it's provider. + */ +- fun renewRegistration() ++// fun renewRegistration() + + companion object { + /** +diff --git a/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushSubscription.kt b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushSubscription.kt +new file mode 100644 +index 000000000000..470a1145a298 +--- /dev/null ++++ b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushSubscription.kt +@@ -0,0 +1,19 @@ ++/* 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.concept.push ++ ++typealias AppServerKey = String ++typealias PushScope = String ++ ++/** ++ * The subscription information from the WebPush server that can be used to send push messages. ++ */ ++data class PushSubscription( ++ val scope: PushScope, ++ val endpoint: String, ++ val publicKey: String, ++ val authKey: String, ++ val appServerKey: AppServerKey?, ++) +diff --git a/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushSubscriptionProcessor.kt b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushSubscriptionProcessor.kt +new file mode 100644 +index 000000000000..95392ec543b7 +--- /dev/null ++++ b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/PushSubscriptionProcessor.kt +@@ -0,0 +1,73 @@ ++/* 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.concept.push ++ ++/** ++ * A push subscriptions processor that handles (un)subscriptions for a given scope. ++ */ ++interface PushSubscriptionProcessor { ++ ++ /** ++ * Start the push processor and any service associated. ++ */ ++ fun initialize() ++ ++ /** ++ * Removes all push subscriptions from the device. ++ */ ++ fun shutdown() ++ ++ /** ++ * Requests the PushSubscriptionProcessor to renew its registration with its provider. ++ */ ++ fun renewRegistration() ++ ++ /** ++ * Run [block] with all registered [PushScope] and appServerKey ++ */ ++ fun forEachScopes(block: (PushScope, String?) -> Unit) ++ ++ /** ++ * Subscribes for push notifications and invokes the [onSubscribe] callback with the subscription information. ++ * ++ * @param scope The subscription identifier which usually represents the website's URI. ++ * @param appServerKey An optional key provided by the application server. ++ * @param onSubscribeError The callback invoked with an [Exception] if the call does not successfully complete. ++ * @param onSubscribe The callback invoked when a subscription for the [scope] is created. ++ */ ++ fun subscribe( ++ scope: PushScope, ++ appServerKey: String? = null, ++ onSubscribeError: (Exception) -> Unit = {}, ++ onSubscribe: ((PushSubscription) -> Unit) = {}, ++ ) ++ ++ /** ++ * Un-subscribes from a valid subscription and invokes the [onUnsubscribe] callback with the result. ++ * ++ * @param scope The subscription identifier which usually represents the website's URI. ++ * @param onUnsubscribeError The callback invoked with an [Exception] if the call does not successfully complete. ++ * @param onUnsubscribe The callback invoked when a subscription for the [scope] is removed. ++ */ ++ fun unsubscribe( ++ scope: PushScope, ++ onUnsubscribeError: (Exception) -> Unit = {}, ++ onUnsubscribe: (Boolean) -> Unit = {}, ++ ) ++ ++ /** ++ * Checks if a subscription for the [scope] already exists. ++ * ++ * @param scope The subscription identifier which usually represents the website's URI. ++ * @param appServerKey An optional key provided by the application server. ++ * @param block The callback invoked when a subscription for the [scope] is found, otherwise null. Note: this will ++ * not execute on the calls thread. ++ */ ++ fun getSubscription( ++ scope: PushScope, ++ appServerKey: String? = null, ++ block: (PushSubscription?) -> Unit, ++ ) ++} +diff --git a/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/Pusher.kt b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/Pusher.kt +new file mode 100644 +index 000000000000..802364f0a9ab +--- /dev/null ++++ b/mobile/android/android-components/components/concept/push/src/main/java/mozilla/components/concept/push/Pusher.kt +@@ -0,0 +1,19 @@ ++/* 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.concept.push ++ ++import mozilla.components.support.base.observer.Observable ++import mozilla.components.support.base.observer.ObserverRegistry ++ ++/** ++ * Push features implementing a [Pusher] handle registrations and messages from a Service, ++ * They handle subscriptions for scopes, and are observers for update to these subscriptions. ++ */ ++abstract class Pusher : PushSubscriptionProcessor, Observable by ObserverRegistry() { ++ /** ++ * A flag to disable our rate-limit logic. This is useful when debugging. ++ */ ++ abstract val disableRateLimit: Boolean ++} +diff --git a/mobile/android/android-components/components/feature/accounts-push/build.gradle b/mobile/android/android-components/components/feature/accounts-push/build.gradle +index 613e5aa1fdd4..4322461f50a2 100644 +--- a/mobile/android/android-components/components/feature/accounts-push/build.gradle ++++ b/mobile/android/android-components/components/feature/accounts-push/build.gradle +@@ -33,6 +33,7 @@ dependencies { + implementation project(':support-base') + implementation project(':concept-push') + implementation project(':feature-push') ++ implementation project(':feature-unifiedpush') + + implementation libs.androidx.work.runtime + implementation libs.androidx.lifecycle.process +diff --git a/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/FxaPushSupportFeature.kt b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/FxaPushSupportFeature.kt +index 92634857c5..92d0a0962a 100644 +--- a/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/FxaPushSupportFeature.kt ++++ b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/FxaPushSupportFeature.kt +@@ -25,15 +25,17 @@ 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 + import mozilla.components.service.fxa.manager.FxaAccountManager + 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 mozilla.components.concept.sync.AccountObserver as SyncAccountObserver ++import mozilla.components.concept.push.PushObserver ++import mozilla.components.concept.push.PushScope ++import mozilla.components.concept.push.PushSubscription ++import mozilla.components.concept.push.PushSubscriptionProcessor ++import mozilla.components.concept.push.Pusher + + internal const val PREFERENCE_NAME = "mozac_feature_accounts_push" + internal const val PREF_LAST_VERIFIED = "last_verified_push_subscription" +@@ -56,7 +58,7 @@ internal const val PREF_FXA_SCOPE = "fxa_push_scope" + class FxaPushSupportFeature( + private val context: Context, + private val accountManager: FxaAccountManager, +- private val pushFeature: AutoPushFeature, ++ private val pushFeature: Pusher, + private val crashReporter: CrashReporting? = null, + private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), + private val owner: LifecycleOwner = ProcessLifecycleOwner.get(), +@@ -77,7 +79,7 @@ class FxaPushSupportFeature( + fun initialize() = coroutineScope.launch { + val scopeValue = pushScope.value() + +- val autoPushObserver = AutoPushObserver(accountManager, pushFeature, scopeValue) ++ val fxaPushObserver = FxaPushObserver(accountManager, pushFeature, scopeValue) + + val accountObserver = AccountObserver( + context, +@@ -91,7 +93,7 @@ class FxaPushSupportFeature( + coroutineScope.launch(Main) { + accountManager.register(accountObserver) + +- pushFeature.register(autoPushObserver, owner, autoPause) ++ pushFeature.register(fxaPushObserver, owner, autoPause) + } + } + +@@ -106,7 +108,7 @@ class FxaPushSupportFeature( + */ + internal class AccountObserver( + private val context: Context, +- private val push: AutoPushFeature, ++ private val push: Pusher, + private val fxaPushScope: String, + private val crashReporter: CrashReporting?, + private val lifecycleOwner: LifecycleOwner, +@@ -114,7 +116,7 @@ internal class AccountObserver( + ) : SyncAccountObserver { + + private val logger = Logger(AccountObserver::class.java.simpleName) +- private val verificationDelegate = VerificationDelegate(context, push.config.disableRateLimit) ++ private val verificationDelegate = VerificationDelegate(context, push.disableRateLimit) + + @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage + override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { +@@ -153,7 +155,7 @@ internal class AccountObserver( + * it's OK to call this somewhat frequently. + */ + internal fun pushSubscribe( +- push: AutoPushFeature, ++ push: PushSubscriptionProcessor, + account: OAuthAccount, + scope: String, + crashReporter: CrashReporting?, +@@ -196,7 +198,7 @@ internal fun pushSubscribe( + */ + internal class ConstellationObserver( + context: Context, +- private val push: AutoPushFeature, ++ private val push: PushSubscriptionProcessor, + private val scope: String, + private val account: OAuthAccount, + private val verifier: VerificationDelegate = VerificationDelegate(context), +@@ -240,12 +242,12 @@ internal class ConstellationObserver( + /** + * An [AutoPushFeature] observer to handle [FxaAccountManager] subscriptions and push events. + */ +-internal class AutoPushObserver( ++internal class FxaPushObserver( + private val accountManager: FxaAccountManager, +- private val pushFeature: AutoPushFeature, ++ private val pushFeature: PushSubscriptionProcessor, + private val fxaPushScope: String, +-) : AutoPushFeature.Observer { +- private val logger = Logger(AutoPushObserver::class.java.simpleName) ++) : PushObserver { ++ private val logger = Logger(FxaPushObserver::class.java.simpleName) + + override fun onMessageReceived(scope: String, message: ByteArray?) { + if (scope != fxaPushScope) { +@@ -384,7 +386,7 @@ internal data class VerificationState(val timestamp: Long, val totalCount: Int) + + internal fun preference(context: Context) = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + +-internal fun AutoPushSubscription.into() = DevicePushSubscription( ++internal fun PushSubscription.into() = DevicePushSubscription( + endpoint = this.endpoint, + publicKey = this.publicKey, + authKey = this.authKey, +diff --git a/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/PushScopeProperty.kt b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/PushScopeProperty.kt +index cd58a69246..dc35a50c10 100644 +--- a/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/PushScopeProperty.kt ++++ b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/PushScopeProperty.kt +@@ -12,7 +12,7 @@ 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 mozilla.components.concept.push.PushScope + import java.util.UUID + + /** +diff --git a/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/ScopeProperty.kt b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/ScopeProperty.kt +index 30e403088831..532382a2fbbb 100644 +--- a/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/ScopeProperty.kt ++++ b/mobile/android/android-components/components/feature/accounts-push/src/main/java/mozilla/components/feature/accounts/push/cache/ScopeProperty.kt +@@ -4,7 +4,7 @@ + + package mozilla.components.feature.accounts.push.cache + +-import mozilla.components.feature.push.PushScope ++import mozilla.components.concept.push.PushScope + + /** + * A [ScopeProperty] implementation generates and holds the [PushScope]. +diff --git a/mobile/android/android-components/components/feature/push/src/main/java/mozilla/components/feature/push/AutoPushFeature.kt b/mobile/android/android-components/components/feature/push/src/main/java/mozilla/components/feature/push/AutoPushFeature.kt +index e4bf8170af..f22cd6f168 100644 +--- a/mobile/android/android-components/components/feature/push/src/main/java/mozilla/components/feature/push/AutoPushFeature.kt ++++ b/mobile/android/android-components/components/feature/push/src/main/java/mozilla/components/feature/push/AutoPushFeature.kt +@@ -27,15 +27,17 @@ import mozilla.components.concept.push.PushError + import mozilla.components.concept.push.PushProcessor + import mozilla.components.concept.push.PushService + import mozilla.components.support.base.log.logger.Logger +-import mozilla.components.support.base.observer.Observable +-import mozilla.components.support.base.observer.ObserverRegistry + import mozilla.components.support.base.utils.NamedThreadFactory + import java.io.File + import java.util.concurrent.Executors + import kotlin.coroutines.CoroutineContext ++import mozilla.components.concept.push.AppServerKey ++import mozilla.components.concept.push.PushScope ++import mozilla.components.concept.push.PushSubscription ++import mozilla.components.concept.push.Pusher + +-typealias PushScope = String +-typealias AppServerKey = String ++// typealias PushScope = String ++// typealias AppServerKey = String + + /** + * A implementation of a [PushProcessor] that should live as a singleton by being installed +@@ -80,12 +82,12 @@ typealias AppServerKey = String + class AutoPushFeature( + private val context: Context, + private val service: PushService, +- val config: PushConfig, ++ val config: AutoPushConfig, + coroutineContext: CoroutineContext = Executors.newSingleThreadExecutor( + NamedThreadFactory("AutoPushFeature"), + ).asCoroutineDispatcher(), + private val crashReporter: CrashReporting? = null, +-) : PushProcessor, Observable by ObserverRegistry() { ++) : PushProcessor, Pusher() { + + private val logger = Logger("AutoPushFeature") + +@@ -98,6 +100,13 @@ class AutoPushFeature( + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal var connection: PushManagerInterface? = null + ++ override val disableRateLimit: Boolean ++ get() = config.disableRateLimit ++ ++ init { ++ PushProcessor.install(this) ++ } ++ + /** + * Starts the push feature and initialization work needed. Also starts the [PushService] to ensure new messages + * come through. +@@ -139,10 +148,27 @@ class AutoPushFeature( + */ + override fun shutdown() { + withConnection { +- it.unsubscribeAll() ++ try { ++ it.unsubscribeAll() ++ } catch (e: PushApiException) { ++ } + } + } + ++ override fun forEachScopes(block: (PushScope, String?) -> Unit) { ++ // TODO: uncomment once https://github.com/mozilla/application-services/pull/6671 is merged ++ /* ++ val currentConnection = connection ++ currentConnection ?: run { ++ logger.warn("No current connection") ++ } ++ coroutineScope.launch { ++ currentConnection?.listSubscriptionReq()?.forEach { ++ block(it.scope, it.appServerKey) ++ } ++ }*/ ++ } ++ + /** + * New registration tokens are received and sent to the AutoPush server which also performs subscriptions for + * each push type and notifies the subscribers. +@@ -187,11 +213,11 @@ class AutoPushFeature( + * @param onSubscribeError The callback invoked with an [Exception] if the call does not successfully complete. + * @param onSubscribe The callback invoked when a subscription for the [scope] is created. + */ +- fun subscribe( +- scope: String, +- appServerKey: String? = null, +- onSubscribeError: (Exception) -> Unit = {}, +- onSubscribe: ((AutoPushSubscription) -> Unit) = {}, ++ override fun subscribe( ++ scope: PushScope, ++ appServerKey: String?, ++ onSubscribeError: (Exception) -> Unit, ++ onSubscribe: ((PushSubscription) -> Unit), + ) { + withConnection(errorBlock = { exception -> onSubscribeError(exception) }) { + val sub = it.subscribe(scope, appServerKey ?: "") +@@ -206,10 +232,10 @@ class AutoPushFeature( + * @param onUnsubscribeError The callback invoked with an [Exception] if the call does not successfully complete. + * @param onUnsubscribe The callback invoked when a subscription for the [scope] is removed. + */ +- fun unsubscribe( +- scope: String, +- onUnsubscribeError: (Exception) -> Unit = {}, +- onUnsubscribe: (Boolean) -> Unit = {}, ++ override fun unsubscribe( ++ scope: PushScope, ++ onUnsubscribeError: (Exception) -> Unit, ++ onUnsubscribe: (Boolean) -> Unit, + ) { + withConnection(errorBlock = { exception -> onUnsubscribeError(exception) }) { + onUnsubscribe(it.unsubscribe(scope)) +@@ -224,10 +250,10 @@ class AutoPushFeature( + * @param block The callback invoked when a subscription for the [scope] is found, otherwise null. Note: this will + * not execute on the calls thread. + */ +- fun getSubscription( +- scope: String, +- appServerKey: String? = null, +- block: (AutoPushSubscription?) -> Unit, ++ override fun getSubscription( ++ scope: PushScope, ++ appServerKey: String?, ++ block: (PushSubscription?) -> Unit, + ) { + withConnection { + block(it.getSubscription(scope)?.toPushSubscription(scope, appServerKey)) +@@ -289,18 +315,18 @@ class AutoPushFeature( + /** + * Observers that want to receive updates for new subscriptions and messages. + */ +- interface Observer { ++// interface Observer { + + /** + * A subscription for the scope is available. + */ +- fun onSubscriptionChanged(scope: PushScope) = Unit ++// fun onSubscriptionChanged(scope: PushScope) = Unit + + /** + * A messaged has been received for the [scope]. + */ +- fun onMessageReceived(scope: PushScope, message: ByteArray?) = Unit +- } ++// fun onMessageReceived(scope: PushScope, message: ByteArray?) = Unit ++// } + + private fun exceptionHandler(onError: (PushError) -> Unit) = CoroutineExceptionHandler { _, e -> + when (e) { +@@ -352,13 +378,13 @@ enum class Protocol { + /** + * The subscription information from AutoPush that can be used to send push messages to other devices. + */ +-data class AutoPushSubscription( +- val scope: PushScope, +- val endpoint: String, +- val publicKey: String, +- val authKey: String, +- val appServerKey: String?, +-) ++//data class AutoPushSubscription( ++// val scope: PushScope, ++// val endpoint: String, ++// val publicKey: String, ++// val authKey: String, ++// val appServerKey: String?, ++//) + + /** + * Configuration object for initializing the Push Manager with an AutoPush server. +@@ -369,7 +395,7 @@ data class AutoPushSubscription( + * @param serviceType The push services that the AutoPush server supports. + * @param disableRateLimit A flag to disable our rate-limit logic. This is useful when debugging. + */ +-data class PushConfig( ++data class AutoPushConfig( + val senderId: String, + val serverHost: String = "updates.push.services.mozilla.com", + val protocol: Protocol = Protocol.HTTPS, +@@ -402,8 +428,8 @@ private fun Protocol.toRustHttpProtocol(): PushHttpProtocol { + fun SubscriptionResponse.toPushSubscription( + scope: String, + appServerKey: AppServerKey? = null, +-): AutoPushSubscription { +- return AutoPushSubscription( ++): PushSubscription { ++ return PushSubscription( + scope = scope, + endpoint = subscriptionInfo.endpoint, + authKey = subscriptionInfo.keys.auth, +diff --git a/mobile/android/android-components/components/feature/unifiedpush/README.md b/mobile/android/android-components/components/feature/unifiedpush/README.md +new file mode 100644 +index 000000000000..c492a3879873 +--- /dev/null ++++ b/mobile/android/android-components/components/feature/unifiedpush/README.md +@@ -0,0 +1,64 @@ ++# [Android Components](../../../README.md) > Feature > UnifiedPush ++ ++A component that implements push notifications with UnifiedPush. ++ ++## Usage ++ ++We can start the AutoPushFeature to get the subscription info and decrypted push message: ++ ++```kotlin ++val feature = UnifiedPushFeature( ++ context = context, ++ disableRateLimit = true, ++ crashReporter = crashReporter ++) ++ ++// To start the feature and the service. ++feature.initialize() ++ ++// To stop the feature and the service. ++feature.shutdown() ++ ++// To select the user default UnifiedPush distributor ++feature.useDefaultDistributor(activity) { success -> ++ if (success) { ++ // register known instances ++ } ++} ++ ++// To receive the subscription info for all the subscription changes. ++feature.register(object : PushObserver { ++ override fun onSubscriptionChanged(scope: PushScope) { ++ // Handle subscription info here. ++ } ++}) ++ ++// Subscribe for a unique scope (identifier). ++feature.subscribe("push_subscription_scope_id") ++ ++// To receive messages: ++feature.register(object : PushObserver { ++ override fun onMessageReceived(scope: String, message: ByteArray?) { ++ // Handle decrypted message here. ++ } ++}) ++``` ++ ++In order to receive push messages, a [UnifiedPush distributor](https://unifiedpush.org/users/distributors/) needs to be installed on the system. The app register subscriptions to the distributor, which sends back endpoint for the scope. ++ ++For more information, visit . ++ ++### Setting up the dependency ++ ++Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)): ++ ++```Groovy ++implementation "org.mozilla.components:feature-unifiedpush:{latest-version}" ++``` ++ ++## License ++ ++ 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/ ++ +diff --git a/mobile/android/android-components/components/feature/unifiedpush/build.gradle b/mobile/android/android-components/components/feature/unifiedpush/build.gradle +new file mode 100644 +index 000000000000..7358ed93a2a0 +--- /dev/null ++++ b/mobile/android/android-components/components/feature/unifiedpush/build.gradle +@@ -0,0 +1,52 @@ ++/* 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/. */ ++ ++apply plugin: 'com.android.library' ++apply plugin: 'kotlin-android' ++ ++android { ++ defaultConfig { ++ minSdkVersion config.minSdkVersion ++ compileSdk config.compileSdkVersion ++ targetSdkVersion config.targetSdkVersion ++ } ++ ++ buildTypes { ++ release { ++ minifyEnabled false ++ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' ++ } ++ } ++ ++ namespace 'mozilla.components.feature.unifiedpush' ++} ++ ++ ++dependencies { ++ implementation project(':concept-push') ++ ++ implementation ComponentsDependencies.mozilla_appservices_push ++ ++ // Remove when the MessageBus is implemented somewhere else. ++ implementation project(':support-base') ++ implementation project(':ui-icons') ++ ++ implementation libs.kotlin.coroutines ++ implementation libs.androidx.work.runtime ++ implementation libs.androidx.lifecycle.runtime ++ implementation (libs.thirdparty.unifiedpush) { ++ exclude group: "com.google.protobuf", module: "protobuf-java" ++ } ++ ++ testImplementation project(':support-test') ++ ++ testImplementation libs.androidx.test.core ++ testImplementation libs.androidx.test.junit ++ testImplementation libs.testing.robolectric ++ testImplementation libs.testing.coroutines ++} ++ ++apply from: '../../../android-lint.gradle' ++apply from: '../../../publish.gradle' ++ext.configurePublish(config.componentsGroupId, project.name, project.ext.description) +diff --git a/mobile/android/android-components/components/feature/unifiedpush/proguard-rules.pro b/mobile/android/android-components/components/feature/unifiedpush/proguard-rules.pro +new file mode 100644 +index 000000000000..f1b424510da5 +--- /dev/null ++++ b/mobile/android/android-components/components/feature/unifiedpush/proguard-rules.pro +@@ -0,0 +1,21 @@ ++# Add project specific ProGuard rules here. ++# You can control the set of applied configuration files using the ++# proguardFiles setting in build.gradle. ++# ++# For more details, see ++# http://developer.android.com/guide/developing/tools/proguard.html ++ ++# If your project uses WebView with JS, uncomment the following ++# and specify the fully qualified class name to the JavaScript interface ++# class: ++#-keepclassmembers class fqcn.of.javascript.interface.for.webview { ++# public *; ++#} ++ ++# Uncomment this to preserve the line number information for ++# debugging stack traces. ++#-keepattributes SourceFile,LineNumberTable ++ ++# If you keep the line number information, uncomment this to ++# hide the original source file name. ++#-renamesourcefileattribute SourceFile +diff --git a/mobile/android/android-components/components/feature/unifiedpush/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/unifiedpush/src/main/AndroidManifest.xml +new file mode 100644 +index 000000000000..e4fd46229073 +--- /dev/null ++++ b/mobile/android/android-components/components/feature/unifiedpush/src/main/AndroidManifest.xml +@@ -0,0 +1,17 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/DefaultDistributorManager.kt b/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/DefaultDistributorManager.kt +new file mode 100644 +index 000000000000..46609dfd5a2c +--- /dev/null ++++ b/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/DefaultDistributorManager.kt +@@ -0,0 +1,76 @@ ++/* 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.unifiedpush ++ ++import android.app.Activity ++import android.content.Context ++import mozilla.components.concept.push.PushScope ++import org.unifiedpush.android.connector.UnifiedPush ++ ++/** ++ * Default implementation of [DistributorManager] ++ */ ++class DefaultDistributorManager : DistributorManager { ++ ++ /** ++ * Check if the distributor is still installed ++ * ++ * If the distributor has been uninstalled, [UnifiedPushProcessor.onUnregistered] is called for ++ * all known chanel ++ */ ++ override fun isAvailable(context: Context): Boolean { ++ return UnifiedPush.getAckDistributor(context) != null ++ } ++ ++ /** ++ * Register [scope] with [appServerKey] ++ */ ++ override fun registerScope(context: Context, scope: PushScope, appServerKey: String?) { ++ UnifiedPush.register( ++ context, ++ instance = scope, ++ vapid = appServerKey, ++ messageForDistributor = scope ++ ) ++ } ++ ++ /** ++ * Unregister [scope] ++ */ ++ override fun unregisterScope(context: Context, scope: PushScope) { ++ UnifiedPush.unregister(context, instance = scope) ++ } ++ ++ /** ++ * Register a default instance, may be useful when we are linked to a distributor ++ * but we don't have any active subscription yet ++ */ ++ override fun registerDefaultRegistration(context: Context) { ++ UnifiedPush.register( ++ context, ++ messageForDistributor = "Default registration", ++ ) ++ } ++ ++ /** ++ * Use user default UnifiedPush distributor, then call [callback]. ++ * ++ * The callback is called with `true` if we have successfully ++ * selected a distributor. ++ * ++ * If [UnifiedPushProcessor] is already initialized, the user have toggling off/on ++ * UnifiedPush, we register all known instances to the new distributor ++ */ ++ override fun useDefaultDistributor(activity: Activity, callback: (Boolean) -> Unit) { ++ UnifiedPush.tryUseDefaultDistributor(activity, callback) ++ } ++ ++ /** ++ * Remove current distributor ++ */ ++ override fun removeCurrentDistributor(context: Context) { ++ UnifiedPush.removeDistributor(context) ++ } ++} +diff --git a/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/DistributorManager.kt b/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/DistributorManager.kt +new file mode 100644 +index 000000000000..a0302866d83b +--- /dev/null ++++ b/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/DistributorManager.kt +@@ -0,0 +1,52 @@ ++/* 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.unifiedpush ++ ++import android.app.Activity ++import android.content.Context ++import mozilla.components.concept.push.PushScope ++ ++/** ++ * Interface with functions to interact with an UnifiedPush distributor ++ */ ++interface DistributorManager { ++ ++ /** ++ * Check if the distributor is still installed ++ */ ++ fun isAvailable(context: Context): Boolean ++ ++ /** ++ * Register [scope] with [appServerKey] ++ */ ++ fun registerScope(context: Context, scope: PushScope, appServerKey: String?) ++ ++ /** ++ * Unregister [scope] ++ */ ++ fun unregisterScope(context: Context, scope: PushScope) ++ ++ /** ++ * Register a default instance, may be useful when we are linked to a distributor ++ * but we don't have any active subscription yet ++ */ ++ fun registerDefaultRegistration(context: Context) ++ ++ /** ++ * Use user default UnifiedPush distributor, then call [callback]. ++ * ++ * The callback is called with `true` if we have successfully ++ * selected a distributor. ++ * ++ * If [UnifiedPushProcessor] is already initialized, the user have toggling off/on ++ * UnifiedPush, we register all known instances to the new distributor ++ */ ++ fun useDefaultDistributor(activity: Activity, callback: (Boolean) -> Unit) ++ ++ /** ++ * Remove current distributor ++ */ ++ fun removeCurrentDistributor(context: Context) ++} +diff --git a/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/PushReceiver.kt b/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/PushReceiver.kt +new file mode 100644 +index 000000000000..10fb13d77213 +--- /dev/null ++++ b/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/PushReceiver.kt +@@ -0,0 +1,54 @@ ++/* 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.unifiedpush ++ ++import android.content.Context ++import android.content.Intent ++import org.unifiedpush.android.connector.FailedReason ++import org.unifiedpush.android.connector.MessagingReceiver ++import org.unifiedpush.android.connector.data.PushEndpoint ++import org.unifiedpush.android.connector.data.PushMessage ++ ++/** ++ * Implementation of UnifiedPush [MessagingReceiver] ++ */ ++class PushReceiver : MessagingReceiver() { ++ override fun onMessage(context: Context, message: PushMessage, instance: String) { ++ UnifiedPushProcessor.requireInstance.onMessage( ++ scope = instance, ++ message = message, ++ ) ++ } ++ ++ override fun onNewEndpoint(context: Context, endpoint: PushEndpoint, instance: String) { ++ UnifiedPushProcessor.requireInstance.onNewEndpoint( ++ scope = instance, ++ newEndpoint = endpoint, ++ ) ++ } ++ ++ override fun onRegistrationFailed(context: Context, reason: FailedReason, instance: String) { ++ UnifiedPushProcessor.requireInstance.onError(reason.toPushError()) ++ } ++ ++ override fun onUnregistered(context: Context, instance: String) { ++ UnifiedPushProcessor.requireInstance.onUnregistered(scope = instance) ++ } ++ ++ override fun onReceive(context: Context, intent: Intent) { ++ UnifiedPushProcessor.requireInstance.withCoroutine { ++ super.onReceive(context, intent) ++ } ++ } ++ ++ private fun FailedReason.toPushError(): PushError { ++ return when (this) { ++ FailedReason.NETWORK -> PushError.Network("Push Service needs network to register") ++ FailedReason.INTERNAL_ERROR -> PushError.ServiceUnavailable("Unknown error") ++ FailedReason.ACTION_REQUIRED -> PushError.ServiceUnavailable("Push Service waits for a user action") ++ FailedReason.VAPID_REQUIRED -> PushError.Registration("Push Service requires VAPID") ++ } ++ } ++} +diff --git a/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/SubscriptionsDB.kt b/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/SubscriptionsDB.kt +new file mode 100644 +index 000000000000..0dfdc35d52b7 +--- /dev/null ++++ b/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/SubscriptionsDB.kt +@@ -0,0 +1,220 @@ ++/* 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.unifiedpush ++ ++import android.content.ContentValues ++import android.content.Context ++import android.database.Cursor ++import android.database.sqlite.SQLiteDatabase ++import android.database.sqlite.SQLiteOpenHelper ++ ++/** ++ * Database to track known [Subscription] ++ */ ++class SubscriptionsDB(context: Context) : ++ SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { ++ ++ /** ++ * Contains information about a subscription ++ */ ++ data class Subscription( ++ val scope: String, ++ val endpoint: String? = null, ++ val publicKey: String? = null, ++ val authKey: String? = null, ++ val appServerKey: String? = null, ++ ) ++ ++ override fun onCreate(db: SQLiteDatabase) { ++ db.execSQL(CREATE_TABLE_APPS) ++ } ++ ++ override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { ++ throw IllegalStateException("Upgrade not supported") ++ } ++ ++ /** ++ * Add a [Subscription] ++ */ ++ fun add(subscription: Subscription) { ++ val db = writableDatabase ++ val values = ContentValues().apply { ++ put(FIELD_SCOPE, subscription.scope) ++ put(FIELD_ENDPOINT, subscription.endpoint) ++ put(FIELD_PUBKEY, subscription.publicKey) ++ put(FIELD_AUTH, subscription.authKey) ++ put(FIELD_VAPID, subscription.appServerKey) ++ } ++ db.insertWithOnConflict(TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE) ++ } ++ ++ /** ++ * Update the endpoint or the webpush keys of a registration ++ * ++ * @return `true` if a row has been updated ++ */ ++ fun updateEndpoint(scope: String, endpoint: String, publicKey: String, authKey: String): Boolean { ++ val db = writableDatabase ++ val selection = "$FIELD_SCOPE = ? and (" + ++ "$FIELD_ENDPOINT IS NULL or $FIELD_ENDPOINT != ? " + ++ "or $FIELD_PUBKEY IS NULL or $FIELD_PUBKEY != ? " + ++ "or $FIELD_PUBKEY IS NULL or $FIELD_PUBKEY != ? " + ++ "or $FIELD_AUTH IS NULL or $FIELD_AUTH != ?)" ++ val selectionArgs = arrayOf(scope) ++ val values = ContentValues().apply { ++ put(FIELD_ENDPOINT, endpoint) ++ put(FIELD_PUBKEY, publicKey) ++ put(FIELD_AUTH, authKey) ++ } ++ return db.update( ++ TABLE, ++ values, ++ selection, ++ selectionArgs, ++ ) != 0 ++ } ++ ++ /** ++ * Remove the endpoint of a registration ++ */ ++ fun removeEndpoint(scope: String): Boolean { ++ val db = writableDatabase ++ val selection = "$FIELD_SCOPE = ?" ++ val selectionArgs = arrayOf(scope) ++ val values = ContentValues().apply { ++ putNull(FIELD_ENDPOINT) ++ putNull(FIELD_PUBKEY) ++ putNull(FIELD_AUTH) ++ } ++ return db.update( ++ TABLE, ++ values, ++ selection, ++ selectionArgs, ++ ) != 0 ++ } ++ ++ /** ++ * Remove a subscription ++ */ ++ fun remove(scope: String) { ++ val db = writableDatabase ++ val selection = "$FIELD_SCOPE = ?" ++ val selectionArgs = arrayOf(scope) ++ db.delete(TABLE, selection, selectionArgs) ++ } ++ ++ /** ++ * Remove all known subscriptions ++ */ ++ fun removeAll() { ++ val db = writableDatabase ++ val selection = null ++ val selectionArgs = null ++ db.delete(TABLE, selection, selectionArgs) ++ } ++ ++ /** ++ * List all known subscriptions ++ */ ++ fun listSubscriptions(): List { ++ val db = readableDatabase ++ val projection = null ++ return db.query( ++ TABLE, ++ projection, ++ null, ++ null, ++ null, ++ null, ++ null, ++ ).use { cursor -> ++ generateSequence { if (cursor.moveToNext()) cursor else null } ++ .mapNotNull { cursor.subscription() } ++ .toList() ++ } ++ } ++ ++ /** ++ * Get a subscription based on its scope and its vapid key ++ */ ++ fun getSubscription(scope: String, vapid: String?): Subscription? { ++ val db = readableDatabase ++ val projection = null ++ var selection = "$FIELD_SCOPE = ?" ++ val selectionArgs = mutableListOf(scope) ++ vapid?.let { ++ selection += " and $FIELD_VAPID = ?" ++ selectionArgs.add(vapid) ++ } ++ /* ++ vapid is always null at this moment, ++ [onGetSubscription][org.mozilla.fenix.push.WebPushEngineDelegate.onGetSubscription] ++ doesn't pass appServerKey. ++ ++ Once it it supported, we can add: ++ ?: run { ++ selection += "and $FIELD_VAPID IS NULL" ++ } ++ */ ++ return db.query( ++ TABLE, ++ projection, ++ selection, ++ selectionArgs.toTypedArray(), ++ null, ++ null, ++ null, ++ ).use { cursor -> ++ if (cursor.moveToNext()) { ++ cursor.subscription() ++ } else { ++ null ++ } ++ } ++ } ++ ++ private fun Cursor.subscription(): Subscription? { ++ val scopeColumn = this.getColumnIndex(FIELD_SCOPE) ++ val endpointColumn = this.getColumnIndex(FIELD_ENDPOINT) ++ val pubKeyColumn = this.getColumnIndex(FIELD_PUBKEY) ++ val authColumn = this.getColumnIndex(FIELD_AUTH) ++ val vapidColumn = this.getColumnIndex(FIELD_VAPID) ++ ++ val scope = ( ++ if (scopeColumn >= 0) this.getString(scopeColumn) else null ++ ) ?: return null ++ val endpoint = if (endpointColumn >= 0) this.getString(endpointColumn) else null ++ val pubKey = if (pubKeyColumn >= 0) this.getString(pubKeyColumn) else null ++ val auth = if (authColumn >= 0) this.getString(authColumn) else null ++ val vapidKey = if (vapidColumn >= 0) this.getString(vapidColumn) else null ++ return Subscription( ++ scope, ++ endpoint, ++ pubKey, ++ auth, ++ vapidKey, ++ ) ++ } ++ ++ companion object { ++ private const val DB_NAME = "feature_unifiedpush_subscriptions" ++ private const val DB_VERSION = 1 ++ ++ private const val TABLE = "subscriptions" ++ private const val FIELD_SCOPE = "scope" ++ private const val FIELD_ENDPOINT = "endpoint" ++ private const val FIELD_PUBKEY = "pubkey" ++ private const val FIELD_AUTH = "auth" ++ private const val FIELD_VAPID = "vapid" ++ private const val CREATE_TABLE_APPS = "CREATE TABLE $TABLE (" + ++ "$FIELD_SCOPE TEXT," + ++ "$FIELD_ENDPOINT TEXT," + ++ "$FIELD_PUBKEY TEXT," + ++ "$FIELD_AUTH TEXT," + ++ "$FIELD_VAPID TEXT," + ++ "PRIMARY KEY ($FIELD_SCOPE));" ++ } ++} +diff --git a/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/UnifiedPushFeature.kt b/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/UnifiedPushFeature.kt +new file mode 100644 +index 000000000000..ee7703307210 +--- /dev/null ++++ b/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/UnifiedPushFeature.kt +@@ -0,0 +1,372 @@ ++/* 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.unifiedpush ++ ++import android.app.Activity ++import android.content.Context ++import android.database.SQLException ++import kotlinx.coroutines.CoroutineExceptionHandler ++import kotlinx.coroutines.CoroutineScope ++import kotlinx.coroutines.SupervisorJob ++import kotlinx.coroutines.asCoroutineDispatcher ++import kotlinx.coroutines.delay ++import kotlinx.coroutines.flow.MutableSharedFlow ++import kotlinx.coroutines.flow.filter ++import kotlinx.coroutines.flow.firstOrNull ++import kotlinx.coroutines.flow.takeWhile ++import kotlinx.coroutines.launch ++import kotlinx.coroutines.plus ++import kotlinx.coroutines.withTimeoutOrNull ++import mozilla.components.concept.base.crash.CrashReporting ++import mozilla.components.concept.push.PushScope ++import mozilla.components.concept.push.PushSubscription ++import mozilla.components.concept.push.Pusher ++import mozilla.components.support.base.log.logger.Logger ++import mozilla.components.support.base.utils.NamedThreadFactory ++import org.unifiedpush.android.connector.data.PushEndpoint ++import org.unifiedpush.android.connector.data.PushMessage ++import java.util.UUID ++import java.util.concurrent.Executors ++import java.util.concurrent.TimeoutException ++import kotlin.coroutines.CoroutineContext ++import kotlin.time.Duration ++import kotlin.time.Duration.Companion.seconds ++ ++/** ++ * Implementation of a [UnifiedPushProcessor] that should live as a singleton by being installed ++ * in the Application's onCreate. It receives messages from UnifiedPush service and forwards them ++ * to the web engine. ++ * ++ * Implementation of a [Pusher], that receives request from web engine and allow push event to be ++ * Observable ++ * ++ * ```kotlin ++ * class Application { ++ * override fun onCreate() { ++ * val feature = UnifiedPushFeature(context) ++ * } ++ * } ++ * ``` ++ * ++ * Observe for subscription information changes for each registered scope, and for push messages: ++ * ++ * ```kotlin ++ * feature.register(object: PushObserver { ++ * override fun onSubscriptionChanged(scope: PushScope) { } ++ * override fun onMessageReceived(scope: PushScope, message: ByteArray?) { } ++ * }) ++ * ++ * feature.subscribe("push_subscription_scope_id") ++ * ``` ++ * ++ * @param context the application [Context]. ++ * @param disableRateLimit to disable rate-limit logic. ++ * @param coroutineContext An instance of [CoroutineContext] used for executing async push tasks. ++ * @param crashReporter An optional instance of a [CrashReporting]. ++ * @param timeoutSubscriptionCallback Duration until [subscribe] fail if it doesn't receive an endpoint ++ * @param db [SubscriptionsDB] to track known subscription ++ * @param up [DistributorManager] to interact with UnifiedPush distributor, default to [DefaultDistributorManager] ++ */ ++ ++@Suppress("LargeClass") ++class UnifiedPushFeature( ++ private val context: Context, ++ override val disableRateLimit: Boolean = true, ++ coroutineContext: CoroutineContext = Executors.newSingleThreadExecutor( ++ NamedThreadFactory("UnifiedPushFeature"), ++ ).asCoroutineDispatcher(), ++ private val crashReporter: CrashReporting? = null, ++ private val timeoutSubscriptionCallback: Duration = 5.seconds, ++ private val db: SubscriptionsDB = SubscriptionsDB(context), ++ private val up: DistributorManager = DefaultDistributorManager(), ++) : UnifiedPushProcessor, Pusher() { ++ ++ private val logger = Logger("UnifiedPushFeature") ++ ++ private val coroutineScope = CoroutineScope(coroutineContext) + SupervisorJob() + exceptionHandler { onError(it) } ++ ++ private val subscriptionFlow = MutableSharedFlow>() ++ private val ackFlow = MutableSharedFlow() ++ ++ init { ++ UnifiedPushProcessor.install(this) ++ } ++ ++ fun isAvailable(context: Context): Boolean { ++ return up.isAvailable(context) ++ } ++ ++ /** ++ * Starts the push feature and initialization work needed. Register all known instances ++ */ ++ override fun initialize() { ++ coroutineScope.launch { ++ registerKnownInstances(context) ++ } ++ } ++ ++ /** ++ * Un-subscribes from all push message channels and reset [UnifiedPushProcessor]. ++ * ++ * This should only be done on an account logout or app data deletion. ++ */ ++ override fun shutdown() { ++ coroutineScope.launch { ++ db.removeAll() ++ // Removes all instances and the distributor ++ up.removeCurrentDistributor(context) ++ // We can reset UnifiedPushProcessor, because we can't receive any push event anymore ++ UnifiedPushProcessor.reset() ++ } ++ } ++ ++ /** ++ * Run [block] with all registered [PushScope] and appServerKey ++ */ ++ override fun forEachScopes(block: (PushScope, String?) -> Unit) { ++ coroutineScope.launch { ++ db.listSubscriptions().forEach { block(it.scope, it.appServerKey) } ++ } ++ } ++ ++ /** ++ * New endpoint is received for [scope], notify the scope if any subscription parameter is updated ++ */ ++ override fun onNewEndpoint(scope: PushScope, newEndpoint: PushEndpoint) { ++ logger.info("Got new endpoint for $scope") ++ val pubKeySet = newEndpoint.pubKeySet ?: run { ++ logger.warn("Received push endpoint without public keys") ++ return ++ } ++ val subscription = PushSubscription( ++ scope = scope, ++ endpoint = newEndpoint.url, ++ publicKey = pubKeySet.pubKey, ++ authKey = pubKeySet.auth, ++ appServerKey = null, // The VAPID key is retrieved by the subscriber if needed ++ ) ++ val id = UUID.randomUUID() ++ var waitAck = true ++ // If a registration is updated, we notify observer, if a callback isn't already ++ // registered for this scope ++ if (db.updateEndpoint( ++ scope, ++ subscription.endpoint, ++ subscription.publicKey, ++ subscription.authKey, ++ ) ++ ) { ++ coroutineScope.launch { ++ ackFlow ++ .takeWhile { waitAck } ++ .filter { it == id } ++ .firstOrNull() ++ ?: run { ++ logger.info("Notifying about subscription change") ++ notifyObservers { onSubscriptionChanged(scope) } ++ } ++ } ++ } ++ coroutineScope.launch { ++ subscriptionFlow.emit(id to subscription) ++ delay(100) ++ waitAck = false ++ ackFlow.emit(id) ++ } ++ } ++ ++ /** ++ * A new push message has been received. ++ * The message contains the decrypted payload as sent by the ++ * application server. ++ */ ++ override fun onMessage(scope: PushScope, message: PushMessage) { ++ coroutineScope.launch { ++ if (message.decrypted) { ++ logger.info("New push message decrypted.") ++ notifyObservers { onMessageReceived(scope, message.content) } ++ } else { ++ logger.warn("Couldn't decrypt push message.") ++ } ++ } ++ } ++ ++ /** ++ * An error has occurred during the registration. ++ */ ++ override fun onError(error: PushError) { ++ logger.error("${error.javaClass.simpleName} error: $error") ++ crashReporter?.submitCaughtException(error) ++ } ++ ++ /** ++ * The subscription has been removed from the distributor ++ */ ++ override fun onUnregistered(scope: PushScope) { ++ logger.info("Registration for $scope removed") ++ coroutineScope.launch { ++ db.removeEndpoint(scope) ++ } ++ } ++ ++ /** ++ * Run [block] within the Processor coroutine scope ++ */ ++ override fun withCoroutine(block: () -> Unit) { ++ coroutineScope.launch { ++ block() ++ } ++ } ++ ++ /** ++ * Subscribes for push notifications and invokes the [onSubscribe] callback with the subscription information. ++ * ++ * @param scope The subscription identifier which usually represents the website's URI. ++ * @param appServerKey An optional key provided by the application server. ++ * @param onSubscribeError The callback invoked with an [Exception] if the call does not successfully complete. ++ * @param onSubscribe The callback invoked when a subscription for the [scope] is created. ++ */ ++ override fun subscribe( ++ scope: PushScope, ++ appServerKey: String?, ++ onSubscribeError: (Exception) -> Unit, ++ onSubscribe: ((PushSubscription) -> Unit), ++ ) { ++ coroutineScope.launch { ++ try { ++ db.add( ++ SubscriptionsDB.Subscription( ++ scope = scope, ++ appServerKey = appServerKey, ++ ), ++ ) ++ } catch (e: SQLException) { ++ onSubscribeError(e) ++ return@launch ++ } ++ withTimeoutOrNull(timeoutSubscriptionCallback) { ++ subscriptionFlow.filter { sub -> ++ sub.second.scope == scope ++ }.firstOrNull() ++ }?.let { sub -> ++ ackFlow.emit(sub.first) ++ logger.info("Got a registration for the subscription") ++ onSubscribe(sub.second.copy(appServerKey = appServerKey)) ++ } ?: run { ++ logger.info("No registration received for the subscription") ++ onSubscribeError(TimeoutException()) ++ } ++ } ++ coroutineScope.launch { ++ up.registerScope(context, scope, appServerKey) ++ } ++ } ++ ++ /** ++ * Un-subscribes from a valid subscription and invokes the [onUnsubscribe] callback with the result. ++ * ++ * @param scope The subscription identifier which usually represents the website's URI. ++ * @param onUnsubscribeError The callback invoked with an [Exception] if the call does not successfully complete. ++ * @param onUnsubscribe The callback invoked when a subscription for the [scope] is removed. ++ */ ++ override fun unsubscribe( ++ scope: PushScope, ++ onUnsubscribeError: (Exception) -> Unit, ++ onUnsubscribe: (Boolean) -> Unit, ++ ) { ++ coroutineScope.launch { ++ try { ++ db.remove(scope) ++ onUnsubscribe(true) ++ up.unregisterScope(context, scope) ++ } catch (e: SQLException) { ++ onUnsubscribeError(e) ++ } ++ } ++ } ++ ++ /** ++ * Checks if a subscription for the [scope] already exists. ++ * ++ * @param scope The subscription identifier which usually represents the website's URI. ++ * @param appServerKey An optional key provided by the application server. ++ * @param block The callback invoked when a subscription for the [scope] is found, otherwise null. Note: this will ++ * not execute on the calls thread. ++ */ ++ override fun getSubscription( ++ scope: PushScope, ++ appServerKey: String?, ++ block: (PushSubscription?) -> Unit, ++ ) { ++ coroutineScope.launch { ++ block(db.getSubscription(scope, appServerKey)?.toPushSubscription()) ++ } ++ } ++ ++ /** ++ * Re-subscribe all known registrations ++ */ ++ override fun renewRegistration() { ++ logger.warn("Forcing registration renewal.") ++ coroutineScope.launch { ++ registerKnownInstances(context) ++ } ++ } ++ ++ private fun exceptionHandler(onError: (PushError) -> Unit) = CoroutineExceptionHandler { _, e -> ++ when (e) { ++ is SQLException, ++ -> onError(PushError.DB(e, e.message.orEmpty())) ++ else -> logger.warn("Internal error occurred in UnifiedPushFeature.", e) ++ } ++ } ++ ++ private fun registerKnownInstances(context: Context) { ++ up.registerDefaultRegistration(context) ++ db.listSubscriptions().forEach { ++ up.registerScope(context, it.scope, it.appServerKey) ++ } ++ } ++ ++ /** ++ * Use user default UnifiedPush distributor, then call [callback]. ++ * ++ * The callback is called with `true` if we have successfully ++ * selected a distributor. ++ * ++ * If [UnifiedPushProcessor] is already initialized, the user have toggling off/on ++ * UnifiedPush, we register all known instances to the new distributor ++ */ ++ fun useDefaultDistributor(activity: Activity, callback: (Boolean) -> Unit) { ++ up.useDefaultDistributor(activity) { success -> ++ if (success && UnifiedPushProcessor.isInit) { ++ coroutineScope.launch { ++ registerKnownInstances(context) ++ } ++ } ++ callback(success) ++ } ++ } ++ ++ /** ++ * Remove current UnifiedPush distributor ++ */ ++ fun removeCurrentDistributor(context: Context) { ++ up.removeCurrentDistributor(context) ++ } ++} ++ ++fun SubscriptionsDB.Subscription.toPushSubscription(): PushSubscription? { ++ if (endpoint == null || publicKey == null || authKey == null) { ++ return null ++ } ++ return PushSubscription( ++ scope, ++ endpoint, ++ publicKey, ++ authKey, ++ appServerKey, ++ ) ++} +diff --git a/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/UnifiedPushNotification.kt b/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/UnifiedPushNotification.kt +new file mode 100644 +index 000000000000..0db1d27f6aa2 +--- /dev/null ++++ b/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/UnifiedPushNotification.kt +@@ -0,0 +1,65 @@ ++package mozilla.components.feature.unifiedpush ++ ++import android.app.Notification ++import android.app.NotificationChannel ++import android.app.NotificationManager ++import android.content.Context ++import android.os.Build ++import android.os.Build.VERSION.SDK_INT ++import androidx.core.app.NotificationCompat ++import androidx.core.content.getSystemService ++import mozilla.components.support.base.ids.SharedIdsHelper ++import mozilla.components.ui.icons.R as iconsR ++ ++@Suppress("LargeClass") ++object UnifiedPushNotification { ++ private const val NOTIFICATION_CHANNEL_ID = "mozac.feature.unifiedpush.generic" ++ ++ fun getNotificationId(context: Context): Int { ++ return SharedIdsHelper.getIdForTag(context, NOTIFICATION_CHANNEL_ID) ++ } ++ ++ /** ++ * Build the notification to be displayed when the push services have been uninstalled. ++ */ ++ fun createMissingServiceNotification( ++ context: Context, ++ ): Notification { ++ val channelId = ensureChannelExists(context) ++ val title = context.getString(R.string.mozac_feature_unifiedpush_notification_title) ++ val content = context.getString(R.string.mozac_feature_unifiedpush_notification_text) ++ ++ return NotificationCompat.Builder(context, channelId) ++ .setStyle( ++ NotificationCompat.BigTextStyle() ++ .bigText(content) ++ ) ++ .setSmallIcon(iconsR.drawable.mozac_ic_warning_24) ++ .setContentTitle(title) ++ .setContentText(content) ++ .setPriority(NotificationCompat.PRIORITY_DEFAULT) ++ .build() ++ } ++ ++ ++ /** ++ * Make sure a notification channel for download notification exists. ++ * ++ * Returns the channel id to be used for download notifications. ++ */ ++ private fun ensureChannelExists(context: Context): String { ++ if (SDK_INT >= Build.VERSION_CODES.O) { ++ val notificationManager: NotificationManager = context.getSystemService()!! ++ ++ val channel = NotificationChannel( ++ NOTIFICATION_CHANNEL_ID, ++ context.applicationContext.getString(R.string.mozac_feature_unifiedpush_notification_channel), ++ NotificationManager.IMPORTANCE_DEFAULT, ++ ) ++ ++ notificationManager.createNotificationChannel(channel) ++ } ++ ++ return NOTIFICATION_CHANNEL_ID ++ } ++} +\ No newline at end of file +diff --git a/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/UnifiedPushProcessor.kt b/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/UnifiedPushProcessor.kt +new file mode 100644 +index 000000000000..bf7074acb27f +--- /dev/null ++++ b/mobile/android/android-components/components/feature/unifiedpush/src/main/java/mozilla/components/feature/unifiedpush/UnifiedPushProcessor.kt +@@ -0,0 +1,84 @@ ++/* 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.unifiedpush ++ ++import mozilla.components.concept.push.PushScope ++import org.unifiedpush.android.connector.data.PushEndpoint ++import org.unifiedpush.android.connector.data.PushMessage ++ ++/** ++ * A push notification processor that handles subscription and new messages from the UnifiedPush Service. ++ * Starting Push in the Application's onCreate is recommended. ++ */ ++interface UnifiedPushProcessor { ++ ++ /** ++ * New endpoint is received for [scope], notify the scope if any subscription parameter is updated ++ */ ++ fun onNewEndpoint(scope: PushScope, newEndpoint: PushEndpoint) ++ ++ /** ++ * A new push message has been received. ++ * The message contains the decrypted payload as sent by the ++ * application server. ++ */ ++ fun onMessage(scope: PushScope, message: PushMessage) ++ ++ /** ++ * An error has occurred during the registration. ++ */ ++ fun onError(error: PushError) ++ ++ /** ++ * The subscription has been removed from the distributor ++ */ ++ fun onUnregistered(scope: PushScope) ++ ++ /** ++ * Run [block] within the Processor coroutine scope ++ */ ++ fun withCoroutine(block: ()->Unit) ++ ++ companion object { ++ /** ++ * Initialize and installs the UnifiedPushProcessor into the application. ++ * This needs to be called in the application's onCreate before a push service has started. ++ */ ++ fun install(processor: UnifiedPushProcessor) { ++ instance = processor ++ } ++ ++ val isInit: Boolean ++ get() = instance != null ++ ++ @Volatile ++ private var instance: UnifiedPushProcessor? = null ++ ++ internal fun reset() { ++ instance = null ++ } ++ val requireInstance: UnifiedPushProcessor ++ get() = instance ?: throw IllegalStateException( ++ "You need to call UnifiedPushProcessor.install() on your Push instance from Application.onCreate().", ++ ) ++ } ++} ++ ++/** ++ * Various error types. ++ */ ++sealed class PushError(override val message: String) : Exception() { ++ data class Registration(override val message: String) : PushError(message) ++ data class Network(override val message: String) : PushError(message) ++ ++ /** ++ * @property cause Original exception from DB code. ++ */ ++ data class DB( ++ override val cause: Throwable?, ++ override val message: String = cause?.message.orEmpty(), ++ ) : PushError(message) ++ data class ServiceUnavailable(override val message: String) : PushError(message) ++} +diff --git a/mobile/android/android-components/components/feature/unifiedpush/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/unifiedpush/src/main/res/values/strings.xml +new file mode 100644 +index 0000000000..b92a246afe +--- /dev/null ++++ b/mobile/android/android-components/components/feature/unifiedpush/src/main/res/values/strings.xml +@@ -0,0 +1,12 @@ ++ ++ ++ ++ ++ UnifiedPush ++ ++ UnifiedPush ++ ++ The push service application is no longer available. It may have been uninstalled. Please re-enable UnifiedPush to select a new service. ++ +diff --git a/mobile/android/android-components/components/feature/unifiedpush/src/test/java/mozilla/components/feature/unifiedpush/UnifiedPushFeatureTest.kt b/mobile/android/android-components/components/feature/unifiedpush/src/test/java/mozilla/components/feature/unifiedpush/UnifiedPushFeatureTest.kt +new file mode 100644 +index 000000000000..67904440715e +--- /dev/null ++++ b/mobile/android/android-components/components/feature/unifiedpush/src/test/java/mozilla/components/feature/unifiedpush/UnifiedPushFeatureTest.kt +@@ -0,0 +1,435 @@ ++/* 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.unifiedpush ++ ++import android.database.SQLException ++import androidx.lifecycle.Lifecycle ++import androidx.lifecycle.LifecycleOwner ++import androidx.test.ext.junit.runners.AndroidJUnit4 ++import kotlinx.coroutines.ExperimentalCoroutinesApi ++import kotlinx.coroutines.delay ++import mozilla.components.concept.base.crash.CrashReporting ++import mozilla.components.concept.push.PushObserver ++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 mozilla.components.support.test.rule.runTestOnMain ++import mozilla.components.support.test.whenever ++import org.junit.Assert.assertEquals ++import org.junit.Assert.assertFalse ++import org.junit.Assert.assertNull ++import org.junit.Assert.assertTrue ++import org.junit.Rule ++import org.junit.Test ++import org.junit.runner.RunWith ++import org.mockito.ArgumentMatchers.anyString ++import org.mockito.Mockito.never ++import org.mockito.Mockito.verify ++import org.unifiedpush.android.connector.data.PublicKeySet ++import org.unifiedpush.android.connector.data.PushEndpoint ++import org.unifiedpush.android.connector.data.PushMessage ++import kotlin.time.Duration.Companion.milliseconds ++ ++@ExperimentalCoroutinesApi ++@RunWith(AndroidJUnit4::class) ++class UnifiedPushFeatureTest { ++ ++ @get:Rule ++ val coroutinesTestRule = MainCoroutineRule() ++ ++ private val db: SubscriptionsDB = mock() ++ private val up: DistributorManager = mock() ++ ++ @Test ++ fun `initialize registers known scopes`() = runTestOnMain { ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ db = db, ++ up = up, ++ ) ++ val scope = "testScope" ++ whenever(db.listSubscriptions()).thenReturn( ++ listOf( ++ SubscriptionsDB.Subscription( ++ scope = scope, ++ endpoint = "endpoint", ++ publicKey = "pub", ++ authKey = "auth", ++ appServerKey = null, ++ ), ++ ), ++ ) ++ ++ feature.initialize() ++ verify(up).registerScope(testContext, scope, null) ++ } ++ ++ @Test ++ fun `shutdown removes and unsubscribes all, reset UnifiedPushProcessor`() = runTestOnMain { ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ db = db, ++ up = up, ++ ) ++ feature.shutdown() ++ verify(db).removeAll() ++ verify(up).removeCurrentDistributor(testContext) ++ assertFalse(UnifiedPushProcessor.isInit) ++ } ++ ++ @Test ++ fun `onNewEndpoint updates db`() = runTestOnMain { ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ db = db, ++ ) ++ feature.onNewEndpoint("scope", PushEndpoint("url", PublicKeySet("pub", "auth"))) ++ verify(db).updateEndpoint("scope", "url", "pub", "auth") ++ } ++ ++ @Test ++ fun `onMessage notifies observers`() = runTestOnMain { ++ val message: PushMessage = mock() ++ val owner: LifecycleOwner = mock() ++ val lifecycle: Lifecycle = mock() ++ val observer: PushObserver = mock() ++ whenever(owner.lifecycle).thenReturn(lifecycle) ++ whenever(lifecycle.currentState).thenReturn(Lifecycle.State.STARTED) ++ whenever(message.decrypted) ++ .thenReturn(false) // If we can't decrypt, we shouldn't notify observers. ++ .thenReturn(true) ++ val mockContent = "test".toByteArray() ++ val scope = "testScope" ++ whenever(message.content).thenReturn(mockContent) ++ ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ db = db, ++ ) ++ ++ feature.register(observer) ++ ++ // First we receive a message that can't be decrypted ++ feature.onMessage(scope, message) ++ verify(observer, never()).onMessageReceived("testScope", mockContent) ++ ++ // Then we receive a message that is decrypted ++ feature.onMessage(scope, message) ++ verify(observer).onMessageReceived("testScope", mockContent) ++ } ++ ++ @Test ++ fun `subscribe calls UnifiedPush register`() = runTestOnMain { ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ db = db, ++ up = up, ++ ) ++ ++ val scope1 = "testScope1" ++ val scope2 = "testScope2" ++ val vapid = "vapid" ++ feature.subscribe(scope = scope1, appServerKey = vapid) ++ verify(db).add(any()) ++ verify(up).registerScope(testContext, scope1, vapid) ++ feature.subscribe(scope = scope2) ++ verify(up).registerScope(testContext, scope2, null) ++ } ++ ++ @Test ++ fun `subscribe with new endpoint invokes error callback when it timeouts`() = runTestOnMain { ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ db = db, ++ timeoutSubscriptionCallback = 100.milliseconds, ++ ) ++ var invoked = false ++ var errorInvoked = false ++ ++ feature.subscribe( ++ scope = "testScope", ++ onSubscribeError = { ++ errorInvoked = true ++ }, ++ onSubscribe = { ++ invoked = true ++ }, ++ ) ++ ++ delay(200) ++ verify(db).add(any()) ++ assertFalse(invoked) ++ assertTrue(errorInvoked) ++ } ++ ++ @Test ++ fun `subscribe invokes error callback on database exception`() = runTestOnMain { ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ db = db, ++ ) ++ var invoked = false ++ var errorInvoked = false ++ ++ whenever(db.add(any())).thenAnswer { throw SQLException("") } ++ ++ feature.subscribe( ++ scope = "testScope", ++ appServerKey = null, ++ onSubscribeError = { ++ errorInvoked = true ++ }, ++ onSubscribe = { ++ invoked = true ++ }, ++ ) ++ ++ assertFalse(invoked) ++ assertTrue(errorInvoked) ++ } ++ ++ @Test ++ fun `unsubscribe removes scope, unregister, and call onUnsubscribe`() = runTestOnMain { ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ db = db, ++ up = up, ++ ) ++ var invoked = false ++ var errorInvoked = false ++ val scope = "testScope" ++ ++ feature.unsubscribe( ++ scope = scope, ++ onUnsubscribeError = { ++ errorInvoked = true ++ }, ++ onUnsubscribe = { ++ invoked = true ++ }, ++ ) ++ ++ verify(db).remove(anyString()) ++ verify(up).unregisterScope(testContext, scope) ++ assertTrue(invoked) ++ assertFalse(errorInvoked) ++ } ++ ++ @Test ++ fun `unsubscribe invokes error callback on database exception`() = runTestOnMain { ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ db = db, ++ ) ++ var invoked = false ++ var errorInvoked = false ++ ++ whenever(db.remove(anyString())).thenAnswer { throw SQLException("") } ++ ++ feature.unsubscribe( ++ scope = "testScope", ++ onUnsubscribeError = { ++ errorInvoked = true ++ }, ++ onUnsubscribe = { ++ invoked = true ++ }, ++ ) ++ ++ assertFalse(invoked) ++ assertTrue(errorInvoked) ++ } ++ ++ @Test ++ fun `getSubscription invokes block with null when there is no subscription`() = runTestOnMain { ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ db = db, ++ ) ++ var invoked = false ++ ++ val scope = "testScope" ++ whenever(db.getSubscription(scope, null)).thenReturn(null) ++ ++ feature.getSubscription( ++ scope = scope, ++ appServerKey = null, ++ ) { ++ invoked = it == null ++ } ++ ++ assertTrue(invoked) ++ } ++ ++ @Test ++ fun `getSubscription invokes block when there is a subscription`() = runTestOnMain { ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ db = db, ++ ) ++ var invoked = false ++ ++ val scope = "testScope" ++ whenever(db.getSubscription(scope, null)).thenReturn( ++ SubscriptionsDB.Subscription( ++ scope = scope, ++ endpoint = "endpoint", ++ publicKey = "pub", ++ authKey = "auth", ++ appServerKey = null, ++ ), ++ ) ++ ++ feature.getSubscription( ++ scope = scope, ++ appServerKey = null, ++ ) { ++ invoked = it != null ++ } ++ ++ assertTrue(invoked) ++ } ++ ++ @Test ++ fun `new endpoint without pending subscribe notifies observers when it updates a subscription`() = runTestOnMain { ++ val owner: LifecycleOwner = mock() ++ val lifecycle: Lifecycle = mock() ++ val observers: PushObserver = mock() ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ db = db, ++ ) ++ whenever(owner.lifecycle).thenReturn(lifecycle) ++ whenever(lifecycle.currentState).thenReturn(Lifecycle.State.STARTED) ++ // The registration is updated ++ whenever(db.updateEndpoint(any(), any(), any(), any())) ++ .thenReturn(false) ++ .thenReturn(true) ++ ++ feature.register(observers) ++ ++ // When the new endpoint doesn't update the registration, observers should not be notified ++ feature.onNewEndpoint("scope2", PushEndpoint("url", PublicKeySet("pub", "auth"))) ++ delay(400) ++ verify(observers, never()).onSubscriptionChanged(any()) ++ ++ // When the new endpoint updates the registration, observers should be notified ++ feature.onNewEndpoint("scope2", PushEndpoint("url", PublicKeySet("pub", "auth"))) ++ delay(400) ++ verify(observers).onSubscriptionChanged(any()) ++ } ++ ++ @Test ++ fun `new endpoint with pending subscribe doesn't notify observers`() = runTestOnMain { ++ val owner: LifecycleOwner = mock() ++ val lifecycle: Lifecycle = mock() ++ val observers: PushObserver = mock() ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ db = db, ++ ) ++ whenever(owner.lifecycle).thenReturn(lifecycle) ++ whenever(lifecycle.currentState).thenReturn(Lifecycle.State.STARTED) ++ // The registration is updated ++ whenever(db.updateEndpoint(any(), any(), any(), any())) ++ .thenReturn(true) ++ ++ feature.register(observers) ++ ++ // If there is no pending subscribe, observer is called ++ feature.onNewEndpoint("scope1", PushEndpoint("url", PublicKeySet("pub", "auth"))) ++ delay(400) ++ verify(observers).onSubscriptionChanged("scope1") ++ ++ // When we receive a new endpoint, with a pending subscribe, observers should not be notified ++ feature.subscribe( ++ scope = "scope2", ++ appServerKey = null, ++ ) ++ feature.onNewEndpoint("scope2", PushEndpoint("url", PublicKeySet("pub", "auth"))) ++ delay(400) ++ verify(observers, never()).onSubscriptionChanged("scope2") ++ } ++ ++ @Test ++ fun `crash reporter is notified of errors`() = runTestOnMain { ++ val crashReporter: CrashReporting = mock() ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ crashReporter = crashReporter, ++ ) ++ ++ feature.onError(PushError.Network("Bad things happened!")) ++ ++ verify(crashReporter).submitCaughtException(any()) ++ } ++ ++ @Test ++ fun `DB errors are submitted to crash reporter`() = runTestOnMain { ++ val crashReporter: CrashReporting = mock() ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ crashReporter = crashReporter, ++ up = up, ++ ) ++ ++ whenever(up.registerScope(any(), any(), any())).thenAnswer { throw SQLException("test") } ++ feature.subscribe("123") {} ++ verify(crashReporter).submitCaughtException(any()) ++ } ++ ++ @Test ++ fun `Other errors are not reported`() = runTestOnMain { ++ val crashReporter: CrashReporting = mock() ++ val feature = UnifiedPushFeature( ++ context = testContext, ++ coroutineContext = coroutineContext, ++ crashReporter = crashReporter, ++ up = up, ++ ) ++ ++ whenever(up.registerScope(any(), any(), any())).thenAnswer { throw PushError.Network("test") } ++ feature.subscribe("123") {} ++ verify(crashReporter, never()).submitCaughtException(any()) ++ } ++ ++ @Test ++ fun `transform db subscription to PushSubscription`() { ++ val response = SubscriptionsDB.Subscription( ++ "992a0f0542383f1ea5ef51b7cf4ae6c4", ++ "https://mozilla.com", ++ "123", ++ "456", ++ null, ++ ) ++ val sub = response.toPushSubscription() ++ ++ assertEquals(response.endpoint, sub?.endpoint) ++ assertEquals(response.authKey, sub?.authKey) ++ assertEquals(response.publicKey, sub?.publicKey) ++ assertEquals(response.scope, sub?.scope) ++ assertNull(sub?.appServerKey) ++ ++ val sub2 = response.copy(appServerKey = "key").toPushSubscription() ++ ++ assertEquals("key", sub2?.appServerKey) ++ } ++} +diff --git a/mobile/android/android-components/components/feature/unifiedpush/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/unifiedpush/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +new file mode 100644 +index 000000000000..cf1c399ea81e +--- /dev/null ++++ b/mobile/android/android-components/components/feature/unifiedpush/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +@@ -0,0 +1,2 @@ ++mock-maker-inline ++// This allows mocking final classes (classes are final by default in Kotlin) +diff --git a/mobile/android/android-components/components/feature/unifiedpush/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/unifiedpush/src/test/resources/robolectric.properties +new file mode 100644 +index 000000000000..3f67ea5ac1bf +--- /dev/null ++++ b/mobile/android/android-components/components/feature/unifiedpush/src/test/resources/robolectric.properties +@@ -0,0 +1 @@ ++sdk=35 +diff --git a/mobile/android/fenix/.buildconfig.yml b/mobile/android/fenix/.buildconfig.yml +index b5aa2ce3a4..d863ee1bbf 100644 +--- a/mobile/android/fenix/.buildconfig.yml ++++ b/mobile/android/fenix/.buildconfig.yml +@@ -95,6 +95,7 @@ projects: + - ui-icons + - ui-tabcounter + - ui-widgets ++ - feature-unifiedpush + variants: + - apks: + - abi: arm64-v8a +diff --git a/mobile/android/fenix/app/build.gradle b/mobile/android/fenix/app/build.gradle +index d9a6045423..c8351a14bd 100644 +--- a/mobile/android/fenix/app/build.gradle ++++ b/mobile/android/fenix/app/build.gradle +@@ -654,6 +654,8 @@ dependencies { + implementation project(':lib-dataprotect') + testImplementation project(':support-test-fakes') + ++ implementation project(':feature-unifiedpush') ++ + debugImplementation libs.leakcanary + debugImplementation libs.androidx.compose.ui.tooling + +diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +index 13db9f3c83..526e8f1bf4 100644 +--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt ++++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +@@ -39,7 +39,7 @@ import mozilla.components.browser.storage.sync.GlobalPlacesDependencyProvider + import mozilla.components.concept.base.crash.Breadcrumb + import mozilla.components.concept.engine.webextension.WebExtension + import mozilla.components.concept.engine.webextension.isUnsupported +-import mozilla.components.concept.push.PushProcessor ++//import mozilla.components.concept.push.PushProcessor + import mozilla.components.concept.storage.FrecencyThresholdOption + import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker + import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider +@@ -491,7 +491,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider { + logger.info("AutoPushFeature is configured, initializing it...") + + // Install the AutoPush singleton to receive messages. +- PushProcessor.install(it) ++// PushProcessor.install(it) + + WebPushEngineIntegration(components.core.engine, it).start() + +diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt +index 1e64545a1220..689a28c1888e 100644 +--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt ++++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt +@@ -219,8 +219,8 @@ class BackgroundServices( + accountManager.register(AccountManagerReadyObserver(accountManagerAvailableQueue)) + + // Enable push if it's configured. +- push.feature?.let { autoPushFeature -> +- FxaPushSupportFeature(context, accountManager, autoPushFeature, crashReporter) ++ push.feature?.let { pushFeature -> ++ FxaPushSupportFeature(context, accountManager, pushFeature, crashReporter) + .initialize() + } + +diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Push.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Push.kt +index f99ef999cf..7c59fe8d6d 100644 +--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Push.kt ++++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Push.kt +@@ -8,31 +8,92 @@ import android.content.Context + import androidx.core.net.toUri + import mozilla.components.feature.push.AutoPushFeature + import mozilla.components.feature.push.Protocol +-import mozilla.components.feature.push.PushConfig + import mozilla.components.lib.crash.CrashReporter + import mozilla.components.support.base.log.logger.Logger + import org.mozilla.fenix.R + import org.mozilla.fenix.ext.settings + import org.mozilla.fenix.perf.lazyMonitored + import org.mozilla.fenix.push.FirebasePushService ++import android.app.Activity ++import android.os.StrictMode ++import mozilla.components.concept.push.Pusher ++import mozilla.components.feature.push.AutoPushConfig ++import mozilla.components.feature.unifiedpush.UnifiedPushFeature ++import mozilla.components.feature.unifiedpush.UnifiedPushNotification ++import mozilla.components.support.base.android.NotificationsDelegate ++import org.mozilla.fenix.ext.components ++import org.mozilla.fenix.perf.StrictModeManager ++import org.mozilla.fenix.utils.Settings + + /** + * Component group for push services. These components use services that strongly depend on + * push messaging (e.g. WebPush, SendTab). + */ +-class Push(val context: Context, crashReporter: CrashReporter) { +- val feature by lazyMonitored { +- pushConfig?.let { config -> ++class Push(val context: Context, crashReporter: CrashReporter, val settings: Settings = context.settings(), val strictMode: StrictModeManager = context.components.strictMode, val notificationsDelegate: NotificationsDelegate = context.components.notificationsDelegate, val iAutoPushFeature: AutoPushFeature? = null, val iUnifiedPushFeature: UnifiedPushFeature? = null) { ++ private val logger = Logger("Push") ++ ++ val feature: Pusher? ++ get() = strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { ++ if (settings.useUnifiedPush) { ++ unifiedPushFeature.also { feature -> ++ if (feature.isAvailable(context)) { ++ logger.info("Using UnifiedPush") ++ autoPushFeature?.let { ++ it.initialize() ++ feature.migrateFrom(it) ++ } ++ } else { ++ logger.warn( ++ "UnifiedPush distributor has been uninstalled, " + ++ "the feature is disabled for the moment." ++ ) ++ val notificationId = UnifiedPushNotification.getNotificationId(context) ++ val notification = ++ UnifiedPushNotification.createMissingServiceNotification(context) ++ notificationsDelegate.notify( ++ notificationId = notificationId, ++ notification = notification ++ ) ++ settings.useUnifiedPush = false ++ } ++ } ++ } else { ++ logger.info("Using AutoPush") ++ autoPushFeature?.also { feature -> ++ feature.migrateFrom(unifiedPushFeature) ++ } ++ } ++ } ++ ++ private val autoPushFeature by lazyMonitored { ++ iAutoPushFeature ++ ?: autoPushConfig?.let { config -> + AutoPushFeature( + context = context, +- service = pushService, ++ service = firebasePushService, + config = config, + crashReporter = crashReporter, + ) + } + } + +- private val pushConfig: PushConfig? by lazyMonitored { ++ private val unifiedPushFeature by lazyMonitored { ++ iUnifiedPushFeature ++ ?: UnifiedPushFeature( ++ context = context, ++ disableRateLimit = true, ++ crashReporter = crashReporter, ++ ) ++ } ++ ++ private fun Pusher.migrateFrom(old: Pusher) { ++ old.forEachScopes { scope, vapid -> ++ this.subscribe(scope, vapid) ++ } ++ old.shutdown() ++ } ++ ++ private val autoPushConfig: AutoPushConfig? by lazyMonitored { + val logger = Logger("PushConfig") + val projectIdKey = context.getString(R.string.pref_key_push_project_id) + val resId = context.resources.getIdentifier(projectIdKey, "string", context.packageName) +@@ -43,12 +104,12 @@ class Push(val context: Context, crashReporter: CrashReporter) { + + logger.debug("Creating push configuration for autopush.") + val projectId = context.resources.getString(resId) +- val serverOverride = context.settings().overridePushServer ++ val serverOverride = settings.overridePushServer + if (serverOverride.isEmpty()) { +- PushConfig(projectId) ++ AutoPushConfig(projectId) + } else { + val uri = serverOverride.toUri() +- PushConfig( ++ AutoPushConfig( + projectId, + serverHost = uri.getHost() ?: "", + protocol = if (uri.getScheme() == "http") { +@@ -61,5 +122,13 @@ class Push(val context: Context, crashReporter: CrashReporter) { + } + } + +- private val pushService by lazyMonitored { FirebasePushService() } ++ private val firebasePushService by lazyMonitored { FirebasePushService() } ++ ++ fun switchToUnifiedPush(activity: Activity, callback: (Boolean) -> Unit) { ++ unifiedPushFeature.useDefaultDistributor(activity, callback) ++ } ++ ++ fun switchToAutoPush(context: Context) { ++ unifiedPushFeature.removeCurrentDistributor(context) ++ } + } +diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/push/PushFxaIntegration.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/push/PushFxaIntegration.kt +index 3df203e6e7..27221ce39b 100644 +--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/push/PushFxaIntegration.kt ++++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/push/PushFxaIntegration.kt +@@ -13,12 +13,13 @@ 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 ++import mozilla.components.concept.push.PushObserver ++import mozilla.components.concept.push.PushScope ++import mozilla.components.concept.push.Pusher + + /** + * A lazy initializer for FxaAccountManager if it isn't already initialized. +@@ -54,7 +55,7 @@ import org.mozilla.fenix.components.Push + * assurances, and most importantly, maintainable. + */ + class PushFxaIntegration( +- private val pushFeature: AutoPushFeature, ++ private val pushFeature: Pusher, + lazyAccountManager: Lazy, + ) { + private val observer = +@@ -79,8 +80,8 @@ class PushFxaIntegration( + */ + internal class OneTimePushMessageObserver( + private val lazyAccountManager: Lazy, +- private val pushFeature: AutoPushFeature, +-) : AutoPushFeature.Observer { ++ private val pushFeature: Pusher, ++) : PushObserver { + override fun onMessageReceived(scope: PushScope, message: ByteArray?) { + // Ignore empty push messages. + val rawBytes = message ?: return +diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/push/WebPushEngineIntegration.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/push/WebPushEngineIntegration.kt +index 6681840f10..35aa2dab6c 100644 +--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/push/WebPushEngineIntegration.kt ++++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/push/WebPushEngineIntegration.kt +@@ -12,19 +12,21 @@ import mozilla.components.concept.engine.Engine + import mozilla.components.concept.engine.webpush.WebPushDelegate + import mozilla.components.concept.engine.webpush.WebPushHandler + import mozilla.components.concept.engine.webpush.WebPushSubscription +-import mozilla.components.feature.push.AutoPushFeature +-import mozilla.components.feature.push.AutoPushSubscription +-import mozilla.components.feature.push.PushScope + import mozilla.components.support.base.log.logger.Logger ++import mozilla.components.concept.push.PushObserver ++import mozilla.components.concept.push.PushScope ++import mozilla.components.concept.push.PushSubscription ++import mozilla.components.concept.push.PushSubscriptionProcessor ++import mozilla.components.concept.push.Pusher + + /** + * Engine integration with the push feature to enable WebPush support. + */ + class WebPushEngineIntegration( + private val engine: Engine, +- private val pushFeature: AutoPushFeature, ++ private val pushFeature: Pusher, + private val coroutineScope: CoroutineScope = MainScope(), +-) : AutoPushFeature.Observer { ++) : PushObserver { + + private var handler: WebPushHandler? = null + private val delegate = WebPushEngineDelegate(pushFeature) +@@ -53,7 +55,7 @@ class WebPushEngineIntegration( + } + + internal class WebPushEngineDelegate( +- private val pushFeature: AutoPushFeature, ++ private val pushFeature: PushSubscriptionProcessor, + ) : WebPushDelegate { + private val logger = Logger("WebPushEngineDelegate") + +@@ -95,7 +97,7 @@ internal class WebPushEngineDelegate( + } + } + +-internal fun AutoPushSubscription.toEnginePushSubscription() = WebPushSubscription( ++internal fun PushSubscription.toEnginePushSubscription() = WebPushSubscription( + scope = this.scope, + publicKey = this.publicKey.toDecodedByteArray(), + endpoint = this.endpoint, +diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt +index 43bea16286..1bbc5054b5 100644 +--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt ++++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt +@@ -256,6 +256,12 @@ class SecretSettingsFragment : PreferenceFragmentCompat() { + ) + onPreferenceChangeListener = SharedPreferenceUpdater() + } ++ ++ requirePreference(R.string.pref_key_enable_unifiedpush).apply { ++ isVisible = true ++ isChecked = context.settings().enableUnifiedPush ++ onPreferenceChangeListener = SharedPreferenceUpdater() ++ } + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { +diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +index 325e5d2141..44a6263c5b 100644 +--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt ++++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +@@ -73,6 +73,7 @@ import org.mozilla.fenix.snackbar.SnackbarBinding + import org.mozilla.fenix.utils.Settings + import kotlin.system.exitProcess + import org.mozilla.fenix.GleanMetrics.Settings as SettingsMetrics ++import org.mozilla.fenix.components.Push + + @Suppress("LargeClass", "TooManyFunctions") + class SettingsFragment : PreferenceFragmentCompat() { +@@ -409,6 +410,47 @@ class SettingsFragment : PreferenceFragmentCompat() { + null + } + ++ resources.getString(R.string.pref_key_use_unifiedpush) -> { ++ val context = requireActivity() ++ context.settings().apply { useUnifiedPush = !useUnifiedPush } ++ val alert = AlertDialog.Builder(context).apply { ++ setTitle(context.getString(R.string.preferences_unifiedpush)) ++ setMessage(context.getString(R.string.quit_application)) ++ setNegativeButton(android.R.string.cancel) { dialog: DialogInterface, _ -> ++ dialog.cancel() ++ } ++ ++ setPositiveButton(android.R.string.ok) { _, _ -> ++ Toast.makeText( ++ context, ++ getString(R.string.toast_change_unifiedpush_done), ++ Toast.LENGTH_LONG, ++ ).show() ++ ++ Handler(Looper.getMainLooper()).postDelayed( ++ { ++ exitProcess(0) ++ }, ++ DEFAULT_EXIT_DELAY, ++ ) ++ } ++ create().withCenterAlignedButtons() ++ } ++ if (context.settings().useUnifiedPush) { ++ requireComponents.push.switchToUnifiedPush(context) { success -> ++ if (!success) { ++ context.settings().useUnifiedPush = false ++ } else { ++ alert.show() ++ } ++ } ++ } else { ++ requireComponents.push.switchToAutoPush(context) ++ alert.show() ++ } ++ null ++ } ++ + // Only displayed when secret settings are enabled + resources.getString(R.string.pref_key_override_amo_collection) -> { + val context = requireContext() +@@ -547,6 +589,12 @@ class SettingsFragment : PreferenceFragmentCompat() { + findPreference(getPreferenceKey(R.string.pref_key_start_profiler)) + + with(requireContext().settings()) { ++ findPreference( ++ getPreferenceKey(R.string.pref_key_use_unifiedpush), ++ )?.apply { ++ isVisible = enableUnifiedPush ++ isChecked = useUnifiedPush ++ } + findPreference( + getPreferenceKey(R.string.pref_key_nimbus_experiments), + )?.isVisible = showSecretDebugMenuThisSession +@@ -787,5 +835,6 @@ class SettingsFragment : PreferenceFragmentCompat() { + private const val SCROLL_INDICATOR_DELAY = 10L + private const val FXA_SYNC_OVERRIDE_EXIT_DELAY = 2000L + private const val AMO_COLLECTION_OVERRIDE_EXIT_DELAY = 3000L ++ private const val DEFAULT_EXIT_DELAY = 2000L + } + } +diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +index 7f19ba4304..1e8f963174 100644 +--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt ++++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +@@ -1982,6 +1982,16 @@ class Settings(private val appContext: Context) : PreferencesHolder { + default = 0, + ) + ++ var enableUnifiedPush by booleanPreference( ++ key = appContext.getPreferenceKey(R.string.pref_key_enable_unifiedpush), ++ default = true, ++ ) ++ ++ var useUnifiedPush by booleanPreference( ++ key = appContext.getPreferenceKey(R.string.pref_key_use_unifiedpush), ++ default = false, ++ ) ++ + /** + * Indicates if the Compose Homepage is enabled. + */ +diff --git a/mobile/android/fenix/app/src/main/res/values/preference_keys.xml b/mobile/android/fenix/app/src/main/res/values/preference_keys.xml +index a55bf0589b17..0e3add9a420e 100644 +--- a/mobile/android/fenix/app/src/main/res/values/preference_keys.xml ++++ b/mobile/android/fenix/app/src/main/res/values/preference_keys.xml +@@ -414,6 +414,8 @@ + pref_key_enable_recent_searches + pref_key_enable_shortcuts_suggestions + pref_key_enable_composable_toolbar" ++ pref_key_enable_unifiedpush ++ pref_key_use_unifiedpush + + + pref_key_enable_compose_logins +diff --git a/mobile/android/fenix/app/src/main/res/xml/preferences.xml b/mobile/android/fenix/app/src/main/res/xml/preferences.xml +index 4c6b14bf87..204c648354 100644 +--- a/mobile/android/fenix/app/src/main/res/xml/preferences.xml ++++ b/mobile/android/fenix/app/src/main/res/xml/preferences.xml +@@ -184,6 +184,13 @@ + app:iconSpaceReserved="false" + android:title="@string/preferences_external_download_manager" /> + ++ ++ + ++ + () ++ val unifiedPushFeature = mock() ++ // The distributor isn't available anymore ++ whenever(unifiedPushFeature.isAvailable(any())).thenReturn(false) ++ val settings = Settings(testContext) ++ settings.useUnifiedPush = true ++ assertTrue(settings.useUnifiedPush) ++ Push( ++ testContext, ++ crashReporter = mock(), ++ settings = settings, ++ strictMode = TestStrictModeManager(), ++ notificationsDelegate = notificationsDelegate, ++ iUnifiedPushFeature = unifiedPushFeature ++ ).feature ++ assertFalse(settings.useUnifiedPush) ++ verify(notificationsDelegate).notify(any(), anyInt(), any(), any(), any(), eq(false)) ++ } ++ ++ @Test ++ fun `AutoPush subscriptions are migrated to UnifiedPush when UnifiedPush is enabled`() = runTest { ++ val unifiedPushFeature = mock() ++ val autoPushFeature = mock() ++ whenever(unifiedPushFeature.isAvailable(any())).thenReturn(true) ++ whenever(autoPushFeature.forEachScopes(any())).then { i -> ++ i.getArgument<(PushScope, String?) -> Unit>(0)("test", "test") ++ } ++ val settings = Settings(testContext) ++ // UnifiedPush is enabled ++ settings.useUnifiedPush = true ++ assertTrue(settings.useUnifiedPush) ++ Push( ++ testContext, ++ crashReporter = mock(), ++ settings = settings, ++ strictMode = TestStrictModeManager(), ++ notificationsDelegate = mock(), ++ iUnifiedPushFeature = unifiedPushFeature, ++ iAutoPushFeature = autoPushFeature ++ ).feature ++ verify(autoPushFeature).forEachScopes(any()) ++ verify(autoPushFeature).shutdown() ++ verify(unifiedPushFeature).subscribe(eq("test"), eq("test"), any(), any()) ++ } ++ ++ @Test ++ fun `UnifiedPush subscriptions are migrated to AutoPush when UnifiedPush is disabled`() = runTest { ++ val unifiedPushFeature = mock() ++ val autoPushFeature = mock() ++ whenever(unifiedPushFeature.forEachScopes(any())).then { i -> ++ i.getArgument<(PushScope, String?) -> Unit>(0)("test", "test") ++ } ++ val settings = Settings(testContext) ++ // UnifiedPush is disabled ++ settings.useUnifiedPush = false ++ assertFalse(settings.useUnifiedPush) ++ Push( ++ testContext, ++ crashReporter = mock(), ++ settings = settings, ++ strictMode = TestStrictModeManager(), ++ notificationsDelegate = mock(), ++ iUnifiedPushFeature = unifiedPushFeature, ++ iAutoPushFeature = autoPushFeature ++ ).feature ++ verify(unifiedPushFeature).forEachScopes(any()) ++ verify(unifiedPushFeature).shutdown() ++ verify(autoPushFeature).subscribe(eq("test"), eq("test"), any(), any()) ++ } ++} diff --git a/scripts/patches.yaml b/scripts/patches.yaml index b0ef82e..3fa8a14 100644 --- a/scripts/patches.yaml +++ b/scripts/patches.yaml @@ -85,6 +85,13 @@ patches: reason: "To remove unnecessary tracking libraries and dependencies on proprietary services." effect: "Improves privacy and protects the freedom of users." category: "Dependency" + + - file: "unifiedpush.patch" + name: "UnifiedPush" + description: "Adds support for UnifiedPush." + reason: "To allow users to receive notifications without the use of a proprietary Google service." + effect: "Improves usability for users, by allowing them to receive push notifications if desired, and improves freedom, by allowing users to decide which push server/implementation they would like to use." + category: "Dependency" # Privacy - file: "fenix-disable-telemetry.patch" From 555fb28c5d1748d5a0d3d537acb6f0b68c7b90b7 Mon Sep 17 00:00:00 2001 From: celenity Date: Sat, 5 Jul 2025 15:57:02 -0400 Subject: [PATCH 2/5] fix: conflict Signed-off-by: celenity --- patches/tor-spoof-english.patch | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/patches/tor-spoof-english.patch b/patches/tor-spoof-english.patch index df1b48a..1c63cfd 100644 --- a/patches/tor-spoof-english.patch +++ b/patches/tor-spoof-english.patch @@ -60,23 +60,21 @@ index cfbdaba62c..e1c88f77c8 100644 postQuantumKeyExchangeEnabled = FxNimbus.features.pqcrypto.value().postQuantumKeyExchangeEnabled, dohAutoselectEnabled = FxNimbus.features.doh.value().autoselectEnabled, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/advanced/DefaultLocaleSettingsController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/advanced/DefaultLocaleSettingsController.kt -index e387bc4ae0..404c55872a 100644 +index e387bc4ae0..8cde937ec1 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/advanced/DefaultLocaleSettingsController.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/advanced/DefaultLocaleSettingsController.kt -@@ -12,6 +12,7 @@ import mozilla.components.browser.state.store.BrowserStore - import mozilla.components.support.locale.LocaleManager +@@ -13,6 +13,7 @@ import mozilla.components.support.locale.LocaleManager import mozilla.components.support.locale.LocaleUseCases import org.mozilla.fenix.nimbus.FxNimbus -+import org.mozilla.fenix.ext.components import java.util.Locale ++import org.mozilla.fenix.ext.components interface LocaleSettingsController { -@@ -83,5 +84,9 @@ class DefaultLocaleSettingsController( + fun handleLocaleSelected(locale: Locale) +@@ -83,5 +84,7 @@ class DefaultLocaleSettingsController( config.setLocale(locale) config.setLayoutDirection(locale) resources.updateConfiguration(config, resources.displayMetrics) -+ // A slightly hacky way of triggering a `runtime.settings.locales` update, -+ // so that the locales are updated in GeckoView. + val spoofEnglish = context.components.core.engine.settings.spoofEnglish + context.components.core.engine.settings.spoofEnglish = spoofEnglish } From 18053ad72abd04d101f4c7c9fc25c53a0ccf63d7 Mon Sep 17 00:00:00 2001 From: celenity Date: Sat, 5 Jul 2025 16:38:36 -0400 Subject: [PATCH 3/5] fix Signed-off-by: celenity --- patches/disable-nags.patch | 394 ++++++++++++++++++++ patches/fenix-disable-crash-reporting.patch | 254 +++++++++---- 2 files changed, 571 insertions(+), 77 deletions(-) create mode 100644 patches/disable-nags.patch diff --git a/patches/disable-nags.patch b/patches/disable-nags.patch new file mode 100644 index 0000000..366ef57 --- /dev/null +++ b/patches/disable-nags.patch @@ -0,0 +1,394 @@ +diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingFragment.kt +index e5ab956ac3..4d1ef3de39 100644 +--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingFragment.kt ++++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingFragment.kt +@@ -349,7 +349,7 @@ class OnboardingFragment : Fragment() { + } + + private fun enableSearchBarCFRForNewUser() { +- requireContext().settings().shouldShowSearchBarCFR = FxNimbus.features.encourageSearchCfr.value().enabled ++ requireContext().settings().shouldShowSearchBarCFR = false + } + + private fun isNotDefaultBrowser(context: Context) = +diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +index 7f19ba4304..0f29d724a6 100644 +--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt ++++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +@@ -198,7 +198,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { + ) + + val canShowCfr: Boolean +- get() = (System.currentTimeMillis() - lastCfrShownTimeInMillis) > THREE_DAYS_MS ++ get() = false + + var forceEnableZoom by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_accessibility_force_enable_zoom), +@@ -611,10 +611,10 @@ class Settings(private val appContext: Context) : PreferencesHolder { + /** + * Indicates if the user has completed successfully first translation. + */ +- var showFirstTimeTranslation: Boolean by booleanPreference( +- appContext.getPreferenceKey(R.string.pref_key_show_first_time_translation), +- default = true, +- ) ++ var showFirstTimeTranslation: Boolean = false ++// appContext.getPreferenceKey(R.string.pref_key_show_first_time_translation), ++// default = true, ++// ) + + /** + * Indicates if the user wants translations to automatically be offered as a popup of the dialog. +@@ -783,12 +783,12 @@ class Settings(private val appContext: Context) : PreferencesHolder { + * This will lead to a performance regression since that function can be expensive to call. + */ + fun checkIfFenixIsDefaultBrowserOnAppResume(): Boolean { +- val prefKey = appContext.getPreferenceKey(R.string.pref_key_default_browser) +- val isDefaultBrowserNow = isDefaultBrowserBlocking() +- val wasDefaultBrowserOnLastResume = +- this.preferences.getBoolean(prefKey, isDefaultBrowserNow) +- this.preferences.edit { putBoolean(prefKey, isDefaultBrowserNow) } +- return isDefaultBrowserNow && !wasDefaultBrowserOnLastResume ++// val prefKey = appContext.getPreferenceKey(R.string.pref_key_default_browser) ++// val isDefaultBrowserNow = isDefaultBrowserBlocking() ++// val wasDefaultBrowserOnLastResume = ++// this.preferences.getBoolean(prefKey, isDefaultBrowserNow) ++// this.preferences.edit { putBoolean(prefKey, isDefaultBrowserNow) } ++ return false + } + + /** +@@ -800,40 +800,40 @@ class Settings(private val appContext: Context) : PreferencesHolder { + return browsers.isDefaultBrowser + } + +- var reEngagementNotificationShown by booleanPreference( +- appContext.getPreferenceKey(R.string.pref_key_re_engagement_notification_shown), +- default = false, +- ) ++ var reEngagementNotificationShown = true ++// appContext.getPreferenceKey(R.string.pref_key_re_engagement_notification_shown), ++// default = false, ++// ) + + /** + * Check if we should set the re-engagement notification. + */ + fun shouldSetReEngagementNotification(): Boolean { +- return numberOfAppLaunches <= 1 && !reEngagementNotificationShown ++ return false + } + + /** + * Check if we should show the re-engagement notification. + */ + fun shouldShowReEngagementNotification(): Boolean { +- return !reEngagementNotificationShown && !isDefaultBrowserBlocking() ++ return false + } + + /** + * Indicates if the re-engagement notification feature is enabled + */ +- var reEngagementNotificationEnabled by lazyFeatureFlagPreference( +- key = appContext.getPreferenceKey(R.string.pref_key_re_engagement_notification_enabled), +- default = { FxNimbus.features.reEngagementNotification.value().enabled }, +- featureFlag = true, +- ) ++ var reEngagementNotificationEnabled = false ++// key = appContext.getPreferenceKey(R.string.pref_key_re_engagement_notification_enabled), ++// default = { FxNimbus.features.reEngagementNotification.value().enabled }, ++// featureFlag = true, ++// ) + + /** + * Indicates if the re-engagement notification feature is enabled + */ + val reEngagementNotificationType: Int + get() = +- FxNimbus.features.reEngagementNotification.value().type ++ 0 + + val shouldUseAutoBatteryTheme by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_auto_battery_theme), +@@ -893,30 +893,30 @@ class Settings(private val appContext: Context) : PreferencesHolder { + /** + * Indicates if the total cookie protection CRF should be shown. + */ +- var shouldShowEraseActionCFR by lazyFeatureFlagPreference( +- appContext.getPreferenceKey(R.string.pref_key_should_show_erase_action_popup), +- featureFlag = true, +- default = { feltPrivateBrowsingEnabled }, +- ) ++ var shouldShowEraseActionCFR = false ++// appContext.getPreferenceKey(R.string.pref_key_should_show_erase_action_popup), ++// featureFlag = true, ++// default = { feltPrivateBrowsingEnabled }, ++// ) + + /** + * Indicates if the cookie banners CRF should be shown. + */ +- var shouldShowCookieBannersCFR by lazyFeatureFlagPreference( +- appContext.getPreferenceKey(R.string.pref_key_should_show_cookie_banners_action_popup), +- featureFlag = true, +- default = { shouldShowCookieBannerUI }, +- ) ++ var shouldShowCookieBannersCFR = false ++// appContext.getPreferenceKey(R.string.pref_key_should_show_cookie_banners_action_popup), ++// featureFlag = true, ++// default = { shouldShowCookieBannerUI }, ++// ) + +- var shouldShowTabSwipeCFR by booleanPreference( +- appContext.getPreferenceKey(R.string.pref_key_toolbar_tab_swipe_cfr), +- default = false, +- ) ++ var shouldShowTabSwipeCFR = false ++// appContext.getPreferenceKey(R.string.pref_key_toolbar_tab_swipe_cfr), ++// default = false, ++// ) + +- var hasShownTabSwipeCFR by booleanPreference( +- appContext.getPreferenceKey(R.string.pref_key_toolbar_has_shown_tab_swipe_cfr), +- default = false, +- ) ++ var hasShownTabSwipeCFR = true ++// appContext.getPreferenceKey(R.string.pref_key_toolbar_has_shown_tab_swipe_cfr), ++// default = false, ++// ) + + val blockCookiesSelectionInCustomTrackingProtection by stringPreference( + key = appContext.getPreferenceKey(R.string.pref_key_tracking_protection_custom_cookies_select), +@@ -1192,11 +1192,11 @@ class Settings(private val appContext: Context) : PreferencesHolder { + ) + + private val userNeedsToVisitInstallableSites: Boolean +- get() = pwaInstallableVisitCount.underMaxCount() ++ get() = false + + val shouldShowPwaCfr: Boolean + get() { +- if (!canShowCfr) return false ++ return false + // We only want to show this on the 3rd time a user visits a site + if (userNeedsToVisitInstallableSites) return false + +@@ -1212,42 +1212,42 @@ class Settings(private val appContext: Context) : PreferencesHolder { + return !userKnowsAboutPwas + } + +- var userKnowsAboutPwas by booleanPreference( +- appContext.getPreferenceKey(R.string.pref_key_user_knows_about_pwa), +- default = false, +- ) ++ var userKnowsAboutPwas = true ++// appContext.getPreferenceKey(R.string.pref_key_user_knows_about_pwa), ++// default = false, ++// ) + +- var shouldShowOpenInAppBanner by booleanPreference( +- appContext.getPreferenceKey(R.string.pref_key_should_show_open_in_app_banner), +- default = true, +- ) ++ var shouldShowOpenInAppBanner = false ++// appContext.getPreferenceKey(R.string.pref_key_should_show_open_in_app_banner), ++// default = true, ++// ) + + val shouldShowOpenInAppCfr: Boolean +- get() = canShowCfr && shouldShowOpenInAppBanner ++ get() = false + +- var shouldShowAutoCloseTabsBanner by booleanPreference( +- appContext.getPreferenceKey(R.string.pref_key_should_show_auto_close_tabs_banner), +- default = true, +- ) ++ var shouldShowAutoCloseTabsBanner = false ++// appContext.getPreferenceKey(R.string.pref_key_should_show_auto_close_tabs_banner), ++// default = true, ++// ) + +- var shouldShowLockPbmBanner by lazyFeatureFlagPreference( +- appContext.getPreferenceKey(R.string.pref_key_should_show_lock_pbm_banner), +- featureFlag = FxNimbus.features.privateBrowsingLock.value().enabled, +- default = { true }, +- ) ++ var shouldShowLockPbmBanner = false ++// appContext.getPreferenceKey(R.string.pref_key_should_show_lock_pbm_banner), ++// featureFlag = FxNimbus.features.privateBrowsingLock.value().enabled, ++// default = { true }, ++// ) + +- var shouldShowInactiveTabsOnboardingPopup by booleanPreference( +- appContext.getPreferenceKey(R.string.pref_key_should_show_inactive_tabs_popup), +- default = true, +- ) ++ var shouldShowInactiveTabsOnboardingPopup = false ++// appContext.getPreferenceKey(R.string.pref_key_should_show_inactive_tabs_popup), ++// default = true, ++// ) + + /** + * Indicates if the auto-close dialog for inactive tabs has been dismissed before. + */ +- var hasInactiveTabsAutoCloseDialogBeenDismissed by booleanPreference( +- appContext.getPreferenceKey(R.string.pref_key_has_inactive_tabs_auto_close_dialog_dismissed), +- default = false, +- ) ++ var hasInactiveTabsAutoCloseDialogBeenDismissed = true ++// appContext.getPreferenceKey(R.string.pref_key_has_inactive_tabs_auto_close_dialog_dismissed), ++// default = false, ++// ) + + /** + * Indicates if the auto-close dialog should be visible based on +@@ -1256,7 +1256,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { + * and if the auto-close setting is already set to [closeTabsAfterOneMonth]. + */ + fun shouldShowInactiveTabsAutoCloseDialog(numbersOfTabs: Int): Boolean { +- return !hasInactiveTabsAutoCloseDialogBeenDismissed && ++ return false && + numbersOfTabs >= INACTIVE_TAB_MINIMUM_TO_SHOW_AUTO_CLOSE_DIALOG && + !closeTabsAfterOneMonth + } +@@ -1665,11 +1665,11 @@ class Settings(private val appContext: Context) : PreferencesHolder { + /** + * Indicates if sync onboarding CFR should be shown. + */ +- var showSyncCFR by lazyFeatureFlagPreference( +- appContext.getPreferenceKey(R.string.pref_key_should_show_sync_cfr), +- featureFlag = true, +- default = { mr2022Sections[Mr2022Section.SYNC_CFR] == true }, +- ) ++ var showSyncCFR = false ++// appContext.getPreferenceKey(R.string.pref_key_should_show_sync_cfr), ++// featureFlag = true, ++// default = { mr2022Sections[Mr2022Section.SYNC_CFR] == true }, ++// ) + + /** + * Indicates if home onboarding dialog should be shown. +@@ -1883,10 +1883,10 @@ class Settings(private val appContext: Context) : PreferencesHolder { + /** + * Indicates if the search bar CFR should be displayed to the user. + */ +- var shouldShowSearchBarCFR by booleanPreference( +- key = appContext.getPreferenceKey(R.string.pref_key_should_searchbar_cfr), +- default = false, +- ) ++ var shouldShowSearchBarCFR = false ++// key = appContext.getPreferenceKey(R.string.pref_key_should_searchbar_cfr), ++// default = false, ++// ) + + /** + * Indicates whether or not to use remote server search configuration. +@@ -1900,10 +1900,10 @@ class Settings(private val appContext: Context) : PreferencesHolder { + /** + * Indicates if the menu CFR should be displayed to the user. + */ +- var shouldShowMenuCFR by booleanPreference( +- key = appContext.getPreferenceKey(R.string.pref_key_menu_cfr), +- default = true, +- ) ++ var shouldShowMenuCFR = false ++// key = appContext.getPreferenceKey(R.string.pref_key_menu_cfr), ++// default = true, ++// ) + + /** + * Get the current mode for how https-only is enabled. +@@ -2114,10 +2114,10 @@ class Settings(private val appContext: Context) : PreferencesHolder { + /** + * Indicates first time engaging with signup + */ +- var isFirstTimeEngagingWithSignup: Boolean by booleanPreference( +- appContext.getPreferenceKey(R.string.pref_key_first_time_engage_with_signup), +- default = true, +- ) ++ var isFirstTimeEngagingWithSignup: Boolean = false ++// appContext.getPreferenceKey(R.string.pref_key_first_time_engage_with_signup), ++// default = true, ++// ) + + /** + * Indicates if the user has chosen to show sponsored search suggestions in the awesomebar. +@@ -2269,7 +2269,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { + */ + val shouldShowSetAsDefaultPrompt: Boolean + get() = +- (System.currentTimeMillis() - lastSetAsDefaultPromptShownTimeInMillis) > ++ false && (System.currentTimeMillis() - lastSetAsDefaultPromptShownTimeInMillis) > + DAYS_BETWEEN_DEFAULT_BROWSER_PROMPTS * ONE_DAY_MS && + numberOfSetAsDefaultPromptShownTimes < MAX_NUMBER_OF_DEFAULT_BROWSER_PROMPTS && + coldStartsBetweenSetAsDefaultPrompts >= APP_COLD_STARTS_TO_SHOW_DEFAULT_PROMPT +@@ -2430,44 +2430,44 @@ class Settings(private val appContext: Context) : PreferencesHolder { + /** + * Indicates if the user has completed the setup step for choosing the toolbar location + */ +- var hasCompletedSetupStepToolbar by booleanPreference( +- key = appContext.getPreferenceKey(R.string.pref_key_setup_step_toolbar), +- default = false, +- ) ++ var hasCompletedSetupStepToolbar = true ++// key = appContext.getPreferenceKey(R.string.pref_key_setup_step_toolbar), ++// default = false, ++// ) + + /** + * Indicates if the user has completed the setup step for choosing the theme + */ +- var hasCompletedSetupStepTheme by booleanPreference( +- key = appContext.getPreferenceKey(R.string.pref_key_setup_step_theme), +- default = false, +- ) ++ var hasCompletedSetupStepTheme = true ++// key = appContext.getPreferenceKey(R.string.pref_key_setup_step_theme), ++// default = false, ++// ) + + /** + * Indicates if the user has completed the setup step for exploring extensions + */ +- var hasCompletedSetupStepExtensions by booleanPreference( +- key = appContext.getPreferenceKey(R.string.pref_key_setup_step_extensions), +- default = false, +- ) ++ var hasCompletedSetupStepExtensions = true ++// key = appContext.getPreferenceKey(R.string.pref_key_setup_step_extensions), ++// default = false, ++// ) + + /** + * Indicates if this is the default browser. + */ +- var isDefaultBrowser by booleanPreference( +- key = appContext.getPreferenceKey(R.string.pref_key_default_browser), +- default = false, +- ) ++ var isDefaultBrowser = true ++// key = appContext.getPreferenceKey(R.string.pref_key_default_browser), ++// default = false, ++// ) + + /** + * Indicates whether or not to show the checklist feature. + */ +- var showSetupChecklist by lazyFeatureFlagPreference( +- key = appContext.getPreferenceKey(R.string.pref_key_setup_checklist_complete), +- default = { +- FxNimbus.features.setupChecklist.value().enabled && +- canShowAddSearchWidgetPrompt(AppWidgetManager.getInstance(appContext)) +- }, +- featureFlag = true, +- ) ++ var showSetupChecklist = false ++// key = appContext.getPreferenceKey(R.string.pref_key_setup_checklist_complete), ++// default = { ++// FxNimbus.features.setupChecklist.value().enabled && ++// canShowAddSearchWidgetPrompt(AppWidgetManager.getInstance(appContext)) ++// }, ++// featureFlag = true, ++// ) + } diff --git a/patches/fenix-disable-crash-reporting.patch b/patches/fenix-disable-crash-reporting.patch index 1238052..c58775e 100644 --- a/patches/fenix-disable-crash-reporting.patch +++ b/patches/fenix-disable-crash-reporting.patch @@ -1,8 +1,8 @@ diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/worker/Extensions.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/worker/Extensions.kt -index d897b9af6f..2fb1be3150 100644 +index d897b9af6f..086f87d87b 100644 --- a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/worker/Extensions.kt +++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/worker/Extensions.kt -@@ -12,9 +12,5 @@ import java.io.IOException +@@ -12,9 +12,9 @@ import java.io.IOException * Indicates if an exception should be reported to the crash reporter. */ internal fun Exception.shouldReport(): Boolean { @@ -11,50 +11,45 @@ index d897b9af6f..2fb1be3150 100644 - cause !is CancellationException && - this !is CancellationException && - isRecoverable ++// val isRecoverable = (this as? WebExtensionException)?.isRecoverable ?: true + return false ++// cause !is CancellationException && ++// this !is CancellationException && ++// isRecoverable } diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt -index 8efdd24f23..9600f952ed 100644 +index 8efdd24f23..867241ae04 100644 --- a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt -@@ -100,7 +100,6 @@ class GleanCrashReporterService( +@@ -100,7 +100,7 @@ class GleanCrashReporterService( @SerialName("count") data class Count(val label: String) : GleanCrashAction() { override fun submit() { - CrashMetrics.crashCount[label].add() ++// CrashMetrics.crashCount[label].add() } } -@@ -151,11 +150,6 @@ class GleanCrashReporterService( +@@ -151,11 +151,11 @@ class GleanCrashReporterService( val minidumpHash: String?, ) : PingCause() { override fun setMetricsLater() { - GleanCrash.cause.setLater("os_fault") - remoteType?.let { GleanCrash.remoteType.setLater(it) } - minidumpHash?.let { GleanCrash.minidumpSha256Hash.setLater(it) } -- ++// GleanCrash.cause.setLater("os_fault") ++// remoteType?.let { GleanCrash.remoteType.setLater(it) } ++// minidumpHash?.let { GleanCrash.minidumpSha256Hash.setLater(it) } + - extras?.let(::setExtraMetrics) ++// extras?.let(::setExtraMetrics) } private fun setExtraMetrics(extras: JsonObject) { -@@ -336,10 +330,6 @@ class GleanCrashReporterService( - val breadcrumbs: List? = null, - ) : PingCause() { - override fun setMetricsLater() { -- GleanCrash.cause.setLater("java_exception") -- GleanCrash.javaException.setLater( -- Json.decodeFromJsonElement(throwableJson), -- ) - } - } - } -@@ -356,49 +346,6 @@ class GleanCrashReporterService( - val startup: Boolean = false, - ) : GleanCrashAction() { - override fun submit() { -- // Perform all processing up-front in case an unexpected exception occurs. We don't -- // want to partially set Glean metrics. The Glean metric functions do not throw -- // exceptions. +@@ -359,46 +359,46 @@ class GleanCrashReporterService( + // Perform all processing up-front in case an unexpected exception occurs. We don't + // want to partially set Glean metrics. The Glean metric functions do not throw + // exceptions. - val setMetrics = cause.prepareSetMetrics() - var setBreadcrumbs = {} - if (breadcrumbs.isNotEmpty()) { @@ -85,9 +80,39 @@ index 8efdd24f23..9600f952ed 100644 - ) - setBreadcrumbs = { GleanCrash.breadcrumbs.set(value) } - } -- -- // Set all metrics and submit the ping. We are guaranteed to not throw any -- // exceptions here, so the metrics will never be partially set. ++// val setMetrics = cause.prepareSetMetrics() ++// var setBreadcrumbs = {} ++// if (breadcrumbs.isNotEmpty()) { ++// val value = Json.decodeFromJsonElement( ++// JsonArray( ++// breadcrumbs.map { breadcrumb -> ++// JsonObject( ++// mapOf( ++// "timestamp" to JsonPrimitive(breadcrumb.timestamp), ++// "category" to JsonPrimitive(breadcrumb.category), ++// "type" to JsonPrimitive(breadcrumb.type), ++// "level" to JsonPrimitive(breadcrumb.level), ++// "message" to JsonPrimitive(breadcrumb.message), ++// "data" to JsonArray( ++// breadcrumb.data.map { ++// JsonObject( ++// mapOf( ++// "key" to JsonPrimitive(it.key), ++// "value" to JsonPrimitive(it.value), ++// ), ++// ) ++// }, ++// ), ++// ), ++// ) ++// }, ++// ), ++// ) ++// setBreadcrumbs = { GleanCrash.breadcrumbs.set(value) } ++// } + + // Set all metrics and submit the ping. We are guaranteed to not throw any + // exceptions here, so the metrics will never be partially set. - GleanEnvironment.uptime.setRawNanos(uptimeNanos) - GleanCrash.processType.set(processType) - GleanCrash.time.set(Date(timeMillis)) @@ -95,10 +120,26 @@ index 8efdd24f23..9600f952ed 100644 - setMetrics() - setBreadcrumbs() - Pings.crash.submit(reason) ++// GleanEnvironment.uptime.setRawNanos(uptimeNanos) ++// GleanCrash.processType.set(processType) ++// GleanCrash.time.set(Date(timeMillis)) ++// GleanCrash.startup.set(startup) ++// setMetrics() ++// setBreadcrumbs() ++// Pings.crash.submit(reason)*/ } } } -@@ -544,17 +491,6 @@ class GleanCrashReporterService( +@@ -455,7 +455,7 @@ class GleanCrashReporterService( + logger.error("Expected file, but found directory") + false + } else { +- true ++ false + } + } + +@@ -544,17 +544,17 @@ class GleanCrashReporterService( } override fun record(crash: Crash.UncaughtExceptionCrash) { @@ -113,10 +154,21 @@ index 8efdd24f23..9600f952ed 100644 - breadcrumbs = crash.breadcrumbs.map { it.toBreadcrumb() }, - ), - ) ++// recordCrashAction(GleanCrashAction.Count(UNCAUGHT_EXCEPTION_KEY)) ++// recordCrashAction( ++// GleanCrashAction.Ping( ++// uptimeNanos = uptime(), ++// processType = "main", ++// timeMillis = crash.timestamp, ++// reason = Pings.crashReasonCodes.crash, ++// cause = GleanCrashAction.PingCause.JavaException(crash.throwable.toJson()), ++// breadcrumbs = crash.breadcrumbs.map { it.toBreadcrumb() }, ++// ), ++// ) } private fun getExtrasJson(path: String): JsonObject? { -@@ -633,50 +569,8 @@ class GleanCrashReporterService( +@@ -633,50 +633,50 @@ class GleanCrashReporterService( } override fun record(crash: Crash.NativeCodeCrash) { @@ -161,10 +213,52 @@ index 8efdd24f23..9600f952ed 100644 - breadcrumbs = crash.breadcrumbs.map { it.toBreadcrumb() }, - ), - ) ++// when (crash.processVisibility) { ++// Crash.NativeCodeCrash.PROCESS_VISIBILITY_MAIN -> ++// recordCrashAction(GleanCrashAction.Count(MAIN_PROCESS_NATIVE_CODE_CRASH_KEY)) ++// Crash.NativeCodeCrash.PROCESS_VISIBILITY_FOREGROUND_CHILD -> ++// recordCrashAction( ++// GleanCrashAction.Count( ++// FOREGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY, ++// ), ++// ) ++// Crash.NativeCodeCrash.PROCESS_VISIBILITY_BACKGROUND_CHILD -> ++// recordCrashAction( ++// GleanCrashAction.Count( ++// BACKGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY, ++// ), ++// ) ++// } ++ ++// val processType = crash.processType ?: "main" ++ ++// if (crash.minidumpPath != null && crash.extrasPath != null) { ++// MinidumpAnalyzer.load()?.run(crash.minidumpPath, crash.extrasPath, false) ++// } ++ ++// val extrasJson = crash.extrasPath?.let { getExtrasJson(it) } ++ ++// val minidumpHash = crash.minidumpPath?.let { calculateMinidumpHash(it) } ++ ++// recordCrashAction( ++// GleanCrashAction.Ping( ++// uptimeNanos = uptime(), ++// processType = processType, ++// timeMillis = crash.timestamp, ++// reason = Pings.crashReasonCodes.crash, ++// cause = GleanCrashAction.PingCause.OsFault( ++// remoteType = crash.remoteType, ++// extras = extrasJson, ++// minidumpHash = minidumpHash, ++// ), ++// breadcrumbs = crash.breadcrumbs.map { it.toBreadcrumb() }, ++// ), ++// ) } override fun record(throwable: Throwable) { - recordCrashAction(GleanCrashAction.Count(CAUGHT_EXCEPTION_KEY)) ++// recordCrashAction(GleanCrashAction.Count(CAUGHT_EXCEPTION_KEY)) } } diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/MozillaSocorroService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/MozillaSocorroService.kt @@ -216,10 +310,10 @@ index d9a6045423..2f788179ee 100644 try { def token = new File("${rootDir}/.sentry_token").text.trim() diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt -index 13db9f3c83..66ccccf955 100644 +index 13db9f3c83..37fa89c98f 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt -@@ -504,10 +504,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider { +@@ -504,10 +504,10 @@ open class FenixApplication : LocaleAwareApplication(), Provider { } private fun setupCrashReporting() { @@ -227,60 +321,61 @@ index 13db9f3c83..66ccccf955 100644 - .analytics - .crashReporter - .install(this) ++// components ++// .analytics ++// .crashReporter ++// .install(this) } protected open fun initializeNimbus() { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Analytics.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Analytics.kt -index 1ad6a264c7..c9ea9932ca 100644 +index 1ad6a264c7..6cb95a28c0 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Analytics.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Analytics.kt -@@ -16,7 +16,6 @@ import mozilla.components.lib.crash.service.GleanCrashReporterService +@@ -16,7 +16,7 @@ import mozilla.components.lib.crash.service.GleanCrashReporterService import mozilla.components.lib.crash.service.MozillaSocorroService import mozilla.components.support.ktx.android.content.isMainProcess import mozilla.components.support.utils.BrowsersCache -import mozilla.components.support.utils.RunWhenReadyQueue ++//import mozilla.components.support.utils.RunWhenReadyQueue import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.Config import org.mozilla.fenix.HomeActivity -@@ -45,7 +44,6 @@ import org.mozilla.geckoview.BuildConfig.MOZ_UPDATE_CHANNEL +@@ -45,7 +45,7 @@ import org.mozilla.geckoview.BuildConfig.MOZ_UPDATE_CHANNEL */ class Analytics( private val context: Context, - private val runWhenReadyQueue: RunWhenReadyQueue, ++// private val runWhenReadyQueue: RunWhenReadyQueue, ) { val crashReporter: CrashReporter by lazyMonitored { val services = mutableListOf() -@@ -57,7 +55,7 @@ class Analytics( - // we get most value out of nightly/beta logging anyway. - val shouldSendCaughtExceptions = when (Config.channel) { - ReleaseChannel.Release -> false -- else -> true -+ else -> false - } - val sentryService = SentryService( - context, -@@ -74,12 +72,7 @@ class Analytics( +@@ -74,12 +74,12 @@ class Analytics( // We only want to initialize Sentry on startup on the main process. if (context.isMainProcess()) { - runWhenReadyQueue.runIfReadyOrQueue { - sentryService.initIfNeeded() -- } ++// runWhenReadyQueue.runIfReadyOrQueue { ++// sentryService.initIfNeeded() + } } -- + - services.add(sentryService) ++// services.add(sentryService) } // The name "Fenix" here matches the product name on Socorro and is unrelated to the actual app name: -@@ -93,7 +86,6 @@ class Analytics( +@@ -93,7 +93,7 @@ class Analytics( releaseChannel = MOZ_UPDATE_CHANNEL, distributionId = distributionId, ) - services.add(socorroService) ++// services.add(socorroService) val intent = Intent(context, HomeActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP -@@ -119,7 +111,7 @@ class Analytics( +@@ -119,7 +119,7 @@ class Analytics( appName = context.getString(R.string.app_name), organizationName = "Mozilla", ), @@ -289,66 +384,68 @@ index 1ad6a264c7..c9ea9932ca 100644 nonFatalCrashIntent = pendingIntent, useLegacyReporting = !context.settings().crashReportAlwaysSend && !context.settings().useNewCrashReporterDialog, -@@ -161,10 +153,10 @@ class Analytics( +diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt +index 36d4b4c349..f9ff9db3bf 100644 +--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt ++++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt +@@ -104,7 +104,7 @@ private fun Context.reportError(message: String, e: Throwable) { + fun NimbusException.isReportableError(): Boolean { + return when (this) { + is NimbusException.ClientException -> false +- else -> true ++ else -> false } } --private fun isSentryEnabled() = !BuildConfig.SENTRY_TOKEN.isNullOrEmpty() -+private fun isSentryEnabled() = false - - private fun getSentryProjectUrl(): String? { -- val baseUrl = "https://sentry.io/organizations/mozilla/issues" -+ val baseUrl = "" - return when (Config.channel) { - ReleaseChannel.Nightly -> "$baseUrl/?project=6295546" - ReleaseChannel.Release -> "$baseUrl/?project=6375561" diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt -index 6725aa64a0..0fe5231617 100644 +index 6725aa64a0..724b1674cf 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt -@@ -84,13 +84,6 @@ class SearchDialogController( +@@ -84,13 +84,13 @@ class SearchDialogController( } when (url) { - "about:crashes" -> { -- // The list of past crashes can be accessed via "settings > about", but desktop and -- // fennec users may be used to navigating to "about:crashes". So we intercept this here -- // and open the crash list activity instead. ++// "about:crashes" -> { + // The list of past crashes can be accessed via "settings > about", but desktop and + // fennec users may be used to navigating to "about:crashes". So we intercept this here + // and open the crash list activity instead. - activity.startActivity(Intent(activity, CrashListActivity::class.java)) - store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = false)) - } ++// activity.startActivity(Intent(activity, CrashListActivity::class.java)) ++// store.dispatch(AwesomeBarAction.EngagementFinished(abandoned = false)) ++// } "about:addons" -> { val directions = SearchDialogFragmentDirections.actionGlobalAddonsManagementFragment() diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt -index 7f19ba4304..fde794e31b 100644 +index 7f19ba4304..5e64718c0f 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt -@@ -167,8 +167,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { - } +@@ -168,7 +168,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { @VisibleForTesting -- internal val isCrashReportEnabledInBuild: Boolean = + internal val isCrashReportEnabledInBuild: Boolean = - BuildConfig.CRASH_REPORTING && Config.channel.isReleased -+ internal val isCrashReportEnabledInBuild: Boolean = false ++ false override val preferences: SharedPreferences = appContext.getSharedPreferences(FENIX_PREFERENCES, MODE_PRIVATE) -@@ -363,12 +362,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { - default = true, +@@ -364,10 +364,10 @@ class Settings(private val appContext: Context) : PreferencesHolder { ) -- val isCrashReportingEnabled: Boolean + val isCrashReportingEnabled: Boolean - get() = isCrashReportEnabledInBuild && -- preferences.getBoolean( -- appContext.getPreferenceKey(R.string.pref_key_crash_reporter), ++ get() = false && + preferences.getBoolean( + appContext.getPreferenceKey(R.string.pref_key_crash_reporter), - true, -- ) -+ val isCrashReportingEnabled: Boolean = false ++ false, + ) val isRemoteDebuggingEnabled by booleanPreference( - appContext.getPreferenceKey(R.string.pref_key_remote_debugging), -@@ -2310,10 +2304,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { +@@ -2310,10 +2310,10 @@ class Settings(private val appContext: Context) : PreferencesHolder { * A user preference indicating that crash reports should always be automatically sent. This can be updated * through the unsubmitted crash dialog or through data choice preferences. */ @@ -357,6 +454,9 @@ index 7f19ba4304..fde794e31b 100644 - default = false, - ) + var crashReportAlwaysSend = false ++// appContext.getPreferenceKey(R.string.pref_key_crash_reporting_always_report), ++// default = false, ++// ) /** * Indicates whether or not we should use the new crash reporter dialog. From 283e2c8d6a9a39b10f709f882dff6e4a4319f862 Mon Sep 17 00:00:00 2001 From: celenity Date: Sat, 5 Jul 2025 16:55:28 -0400 Subject: [PATCH 4/5] minor fix Signed-off-by: celenity --- patches/fenix-disable-crash-reporting.patch | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/patches/fenix-disable-crash-reporting.patch b/patches/fenix-disable-crash-reporting.patch index c58775e..4774297 100644 --- a/patches/fenix-disable-crash-reporting.patch +++ b/patches/fenix-disable-crash-reporting.patch @@ -329,7 +329,7 @@ index 13db9f3c83..37fa89c98f 100644 protected open fun initializeNimbus() { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Analytics.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Analytics.kt -index 1ad6a264c7..6cb95a28c0 100644 +index 1ad6a264c7..0b80a2807e 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Analytics.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Analytics.kt @@ -16,7 +16,7 @@ import mozilla.components.lib.crash.service.GleanCrashReporterService @@ -356,9 +356,10 @@ index 1ad6a264c7..6cb95a28c0 100644 if (context.isMainProcess()) { - runWhenReadyQueue.runIfReadyOrQueue { - sentryService.initIfNeeded() +- } +// runWhenReadyQueue.runIfReadyOrQueue { +// sentryService.initIfNeeded() - } ++// } } - services.add(sentryService) From 2d62011d933ad039c0f0ca97f05136410fff2703 Mon Sep 17 00:00:00 2001 From: celenity Date: Mon, 7 Jul 2025 19:29:48 -0400 Subject: [PATCH 5/5] Update for v140.0.3 Signed-off-by: celenity --- patches/unifiedpush.patch | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patches/unifiedpush.patch b/patches/unifiedpush.patch index 44122c7..c7982f9 100644 --- a/patches/unifiedpush.patch +++ b/patches/unifiedpush.patch @@ -2577,10 +2577,10 @@ index 325e5d2141..44a6263c5b 100644 } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt -index 7f19ba4304..1e8f963174 100644 +index 9197b274db..420da7a4ab 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt -@@ -1982,6 +1982,16 @@ class Settings(private val appContext: Context) : PreferencesHolder { +@@ -1986,6 +1986,16 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = 0, )