Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ android {
buildConfigField("boolean", "ENABLE_PUSH_NOTIFICATIONS", "$enablePush")
}

// FCM pulls proprietary Google libraries that FOSS repos reject. The foss
// flavour drops them, leaving UnifiedPush as the only transport.
flavorDimensions += "push"
productFlavors {
create("gms")
create("foss")
}

buildTypes {
debug {
enableUnitTestCoverage = true
Expand Down Expand Up @@ -78,8 +86,8 @@ dependencies {
implementation("com.google.android.material:material:1.14.0")
implementation("com.fasterxml.jackson.core:jackson-databind:2.22.1")

implementation(platform("com.google.firebase:firebase-bom:34.16.0"))
implementation("com.google.firebase:firebase-messaging-ktx:24.1.2")
"gmsImplementation"(platform("com.google.firebase:firebase-bom:34.16.0"))
"gmsImplementation"("com.google.firebase:firebase-messaging-ktx:24.1.2")
implementation("com.squareup.okhttp3:okhttp:5.3.2")
implementation("org.unifiedpush.android:connector:3.3.3")
// The connector declares Tink as a runtime-only dependency, so its WebPush
Expand All @@ -89,7 +97,7 @@ dependencies {
// classpath — `tink-android` repackages the same classes and trips AGP's
// duplicate-class check.
implementation("com.google.crypto.tink:tink:1.21.0")
implementation("org.unifiedpush.android:embedded-fcm-distributor:3.0.0")
"gmsImplementation"("org.unifiedpush.android:embedded-fcm-distributor:3.0.0")
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk-android:1.14.11")
testImplementation("io.mockk:mockk-agent:1.14.11")
Expand Down
15 changes: 15 additions & 0 deletions android/src/foss/java/app/tauri/notification/FcmBridge.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package app.tauri.notification

import android.content.Context

object FcmBridge {
fun isConfigured(context: Context): Boolean = false

fun fetchToken(onResult: (FcmTokenResult) -> Unit) {
onResult(FcmTokenResult.Unavailable("FCM is not available in this build"))
}

fun deleteToken(onResult: (FcmDeleteResult) -> Unit) {
onResult(FcmDeleteResult.NotConfigured)
}
}
12 changes: 12 additions & 0 deletions android/src/gms/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<!-- Firebase Cloud Messaging Service for handling push notifications -->
<service
android:name="app.tauri.notification.TauriFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>
40 changes: 40 additions & 0 deletions android/src/gms/java/app/tauri/notification/FcmBridge.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package app.tauri.notification

import android.content.Context
import com.google.firebase.FirebaseApp
import com.google.firebase.messaging.FirebaseMessaging

object FcmBridge {
fun isConfigured(context: Context): Boolean =
try {
FirebaseApp.getApps(context).isNotEmpty()
} catch (_: Exception) {
false
}

fun fetchToken(onResult: (FcmTokenResult) -> Unit) {
try {
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
onResult(
if (task.isSuccessful) FcmTokenResult.Success(task.result)
else FcmTokenResult.Failed("Failed to get FCM token: ${task.exception?.message}")
)
}
} catch (error: Exception) {
onResult(FcmTokenResult.Unavailable(error.message ?: "Failed to get FCM token"))
}
}

fun deleteToken(onResult: (FcmDeleteResult) -> Unit) {
try {
FirebaseMessaging.getInstance().deleteToken().addOnCompleteListener { task ->
onResult(
if (task.isSuccessful) FcmDeleteResult.Deleted
else FcmDeleteResult.Failed("Failed to delete FCM token: ${task.exception?.message}")
)
}
} catch (_: Exception) {
onResult(FcmDeleteResult.NotConfigured)
}
}
}
9 changes: 0 additions & 9 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,6 @@
</intent-filter>
</receiver>

<!-- Firebase Cloud Messaging Service for handling push notifications -->
<service
android:name="app.tauri.notification.TauriFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>

<!-- UnifiedPush connector push service. The connector library's
internal MessagingReceiverImpl receives distributor broadcasts
and forwards them to this service, so no BroadcastReceiver is
Expand Down
19 changes: 19 additions & 0 deletions android/src/main/java/app/tauri/notification/FcmResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package app.tauri.notification

sealed class FcmTokenResult {
data class Success(val token: String) : FcmTokenResult()

/** The request ran and failed. Unlike [Unavailable], this triggers `push-error`. */
data class Failed(val message: String) : FcmTokenResult()

/** Firebase is absent or threw before the request ran. */
data class Unavailable(val message: String) : FcmTokenResult()
}

sealed class FcmDeleteResult {
object Deleted : FcmDeleteResult()
data class Failed(val message: String) : FcmDeleteResult()

/** No default FirebaseApp, so there was nothing to delete. */
object NotConfigured : FcmDeleteResult()
}
61 changes: 23 additions & 38 deletions android/src/main/java/app/tauri/notification/NotificationPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ import app.tauri.plugin.Invoke
import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin
import com.google.firebase.FirebaseApp
import com.google.firebase.messaging.FirebaseMessaging
import org.unifiedpush.android.connector.UnifiedPush
import java.util.ArrayDeque

Expand Down Expand Up @@ -634,12 +632,7 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
getFirebaseToken(registration)
}

private fun fcmConfigured(): Boolean =
try {
FirebaseApp.getApps(activity).isNotEmpty()
} catch (_: Exception) {
false
}
private fun fcmConfigured(): Boolean = FcmBridge.isConfigured(activity)

/** Returns false when no gateway is configured, leaving the caller to report why. */
private fun startEmbeddedPushRegistration(registration: PushRegistration): Boolean {
Expand Down Expand Up @@ -734,18 +727,11 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
return
}

try {
FirebaseMessaging.getInstance().deleteToken().addOnCompleteListener { task ->
if (!task.isSuccessful) {
invoke.reject("Failed to delete FCM token: ${task.exception?.message}")
return@addOnCompleteListener
}
fcmToken = null
if (unifiedPushState.activeProvider == "fcm") unifiedPushState.activeProvider = null
invoke.resolve()
FcmBridge.deleteToken { result ->
if (result is FcmDeleteResult.Failed) {
invoke.reject(result.message)
return@deleteToken
}
} catch (error: Exception) {
// No default FirebaseApp (embedded-FCM/VAPID, no google-services.json): nothing to delete.
fcmToken = null
if (unifiedPushState.activeProvider == "fcm") unifiedPushState.activeProvider = null
invoke.resolve()
Expand Down Expand Up @@ -923,29 +909,28 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
return
}

try {
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (pendingPushRegistration !== registration || registration.phase != PushRegistrationPhase.FCM) {
return@addOnCompleteListener
}
if (!task.isSuccessful) {
val errorMessage = "Failed to get FCM token: ${task.exception?.message}"
val errorData = JSObject()
errorData.put("message", errorMessage)
trigger("push-error", errorData)
finishPushRegistrationError(errorMessage)
return@addOnCompleteListener
}

val token = task.result
fcmToken = token
FcmBridge.fetchToken { outcome ->
if (outcome is FcmTokenResult.Unavailable) {
finishPushRegistrationError(outcome.message)
return@fetchToken
}
if (pendingPushRegistration !== registration || registration.phase != PushRegistrationPhase.FCM) {
return@fetchToken
}
if (outcome is FcmTokenResult.Failed) {
val errorData = JSObject()
errorData.put("message", outcome.message)
trigger("push-error", errorData)
finishPushRegistrationError(outcome.message)
return@fetchToken
}
if (outcome is FcmTokenResult.Success) {
fcmToken = outcome.token
unifiedPushState.activeProvider = "fcm"
val result = JSObject()
result.put("deviceToken", token)
result.put("deviceToken", outcome.token)
finishPushRegistrationSuccess(result)
}
} catch (error: Exception) {
finishPushRegistrationError(error.message ?: "Failed to get FCM token")
}
}

Expand Down
Loading