Add support for Push notifications

This commit is contained in:
sim
2025-08-01 16:23:27 +02:00
parent 6ab0f3222a
commit 9305acc3f1
10 changed files with 165 additions and 29 deletions
@@ -185,6 +185,13 @@
android:foregroundServiceType="mediaPlayback|microphone|camera|remoteMessaging"
/>
<service android:name=".PushService"
android:exported="false">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.PUSH_EVENT"/>
</intent-filter>
</service>
<receiver
android:name=".CallService$CallActionReceiver"
android:enabled="true"
@@ -0,0 +1,82 @@
package chat.simplex.app
import chat.simplex.common.model.CC
import chat.simplex.common.platform.Log
import chat.simplex.common.platform.chatModel
import kotlinx.coroutines.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.unifiedpush.android.connector.FailedReason
import org.unifiedpush.android.connector.PushService
import org.unifiedpush.android.connector.data.PushEndpoint
import org.unifiedpush.android.connector.data.PushMessage
class PushService: PushService() {
private val json = Json {
ignoreUnknownKeys = true
}
@Serializable
data class PNMessage(
val verification: String? = null
)
private fun onVerification(code: String) {
CoroutineScope(Dispatchers.Default).launch {
chatModel.controller.sendCmd(
null,
CC.APIVerifySavedNtf(code),
log = true
)
}
}
override fun onMessage(message: PushMessage, instance: String) {
Log.d(TAG, "onMessage")
val pn: PNMessage = json.decodeFromString(String(message.content))
when {
pn.verification != null -> onVerification(pn.verification)
}
// TODO: Start same job than the periodic service ?
// Receiving the push notif is enough to wake the app and fetch msgs
// But it may not be enough when the phone is in doze, or with some
// vendors
}
override fun onNewEndpoint(endpoint: PushEndpoint, instance: String) {
Log.d(TAG, "onNewEndpoint")
endpoint.pubKeySet ?: run {
// Should not happen
Log.w(TAG, "Missing pubKeySet")
return
}
CoroutineScope(Dispatchers.Default).launch {
chatModel.controller.sendCmd(
null,
CC.APIRegisterWebPush(endpoint.url, endpoint.pubKeySet!!.auth, endpoint.pubKeySet!!.pubKey),
log = true
)
}
}
override fun onRegistrationFailed(reason: FailedReason, instance: String) {
Log.d(TAG, "onRegistrationFailed: $reason")
// TODO: notification to inform about failed registration
}
override fun onUnregistered(instance: String) {
Log.d(TAG, "onUnregistered")
// TODO: notification to inform about unregistration
CoroutineScope(Dispatchers.Default).launch {
chatModel.controller.sendCmd(
null,
CC.APIDeleteSavedNtf(),
log = true
)
}
}
companion object {
private const val TAG = "PushService"
}
}
@@ -16,6 +16,7 @@ import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.getUserServers
import chat.simplex.common.platform.Log
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.networkAndServers.showAddServerDialog
import chat.simplex.res.MR
@@ -39,10 +40,11 @@ object PushManager {
* Else alert about missing service
*/
suspend fun initUnifiedPush(context: Context, scope: CoroutineScope, onSuccess: () -> Unit) {
val userServers = getUserServers(null) ?: listOf()
val rh = chatModel.remoteHostId()
val userServers = getUserServers(rh) ?: listOf()
if (!userServers.hasNtfServer()) {
Log.d(TAG, "User doesn't have any NTF server")
showMissingNTFDialog(scope, userServers)
showMissingNTFDialog(scope, rh, userServers)
// After coming back from the server view, users will have to click on "Instant" again
return
}
@@ -64,7 +66,7 @@ object PushManager {
showSelectPushServiceDialog(context, distributors) {
UnifiedPush.saveDistributor(context, it)
register(context)
onSuccess
onSuccess()
}
}
}
@@ -81,13 +83,13 @@ object PushManager {
*/
private fun List<UserOperatorServers>.hasNtfServer(): Boolean {
// TODO: check if ntf server has a VAPID key
return this.any { it.ntfServers.any() }
return this.any { it.ntfServers.any { s -> s.enabled } }
}
/**
* Show a dialog to inform about missing NTF server
*/
private fun showMissingNTFDialog(scope: CoroutineScope, userServers: List<UserOperatorServers>) = AlertManager.shared.showAlert {
private fun showMissingNTFDialog(scope: CoroutineScope, rh: Long?, userServers: List<UserOperatorServers>) = AlertManager.shared.showAlert {
AlertDialog(
onDismissRequest = AlertManager.shared::hideAlert,
title = {
@@ -109,7 +111,7 @@ object PushManager {
confirmButton = {
TextButton(onClick = {
AlertManager.shared.hideAlert()
showAddServerDialog(scope, userServers)
showAddServerDialog(scope, rh, userServers)
}) { Text(stringResource(MR.strings.smp_servers_add)) }
},
// Ignore
@@ -123,10 +125,10 @@ object PushManager {
/**
* Dialog to add a server, manually or with a QR code
*/
private fun showAddServerDialog(scope: CoroutineScope, userServers: List<UserOperatorServers>) {
private fun showAddServerDialog(scope: CoroutineScope, rh: Long?, userServers: List<UserOperatorServers>) {
val userServersState = mutableStateOf(userServers)
val serverErrors = mutableStateOf(listOf<UserServersError>())
showAddServerDialog(scope, userServersState, serverErrors, null)
showAddServerDialog(scope, userServersState, serverErrors, rh)
}
/**
@@ -3464,6 +3464,9 @@ sealed class CC {
class ResetAgentServersStats(): CC()
class GetAgentSubsTotal(val userId: Long): CC()
class GetAgentServersSummary(val userId: Long): CC()
class APIRegisterWebPush(val endpoint: String, val auth: String, val p256dh: String): CC()
class APIVerifySavedNtf(val code: String): CC()
class APIDeleteSavedNtf(): CC()
val cmdString: String get() = when (this) {
is Console -> cmd
@@ -3656,6 +3659,9 @@ sealed class CC {
is ResetAgentServersStats -> "/reset servers stats"
is GetAgentSubsTotal -> "/get subs total $userId"
is GetAgentServersSummary -> "/get servers summary $userId"
is APIRegisterWebPush -> "/_ntf register webpush $endpoint $auth $p256dh INSTANT"
is APIDeleteSavedNtf -> "/_ntf delete saved"
is APIVerifySavedNtf -> "/_ntf verify $code"
}
val cmdType: String get() = when (this) {
@@ -3814,6 +3820,9 @@ sealed class CC {
is ResetAgentServersStats -> "resetAgentServersStats"
is GetAgentSubsTotal -> "getAgentSubsTotal"
is GetAgentServersSummary -> "getAgentServersSummary"
is APIRegisterWebPush -> "apiRegisterWebPush"
is APIDeleteSavedNtf -> "apiDeleteSavedNtf"
is APIVerifySavedNtf -> "apiVerifySavedNtf"
}
data class ItemRange(val from: Long, val to: Long)
@@ -6952,7 +6961,7 @@ sealed class AgentErrorType {
is CMD -> "CMD ${cmdErr.string} $errContext"
is CONN -> "CONN ${connErr.string}"
is SMP -> "SMP ${smpErr.string}"
// is NTF -> "NTF ${ntfErr.string}"
is NTF -> "NTF ${ntfErr.string}"
is XFTP -> "XFTP ${xftpErr.string}"
is PROXY -> "PROXY $proxyServer $relayServer ${proxyErr.string}"
is RCP -> "RCP ${rcpErr.string}"
@@ -6965,7 +6974,7 @@ sealed class AgentErrorType {
@Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType, val errContext: String): AgentErrorType()
@Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType()
@Serializable @SerialName("SMP") class SMP(val serverAddress: String, val smpErr: SMPErrorType): AgentErrorType()
// @Serializable @SerialName("NTF") class NTF(val ntfErr: SMPErrorType): AgentErrorType()
@Serializable @SerialName("NTF") class NTF(val ntfErr: SMPErrorType): AgentErrorType()
@Serializable @SerialName("XFTP") class XFTP(val xftpErr: XFTPErrorType): AgentErrorType()
@Serializable @SerialName("PROXY") class PROXY(val proxyServer: String, val relayServer: String, val proxyErr: ProxyClientError): AgentErrorType()
@Serializable @SerialName("RCP") class RCP(val rcpErr: RCErrorType): AgentErrorType()
@@ -101,27 +101,26 @@ fun navigateToProtocolView(
userServers = userServers,
serverErrors = serverErrors,
onDelete = {
if (protocol == ServerProtocol.SMP) {
deleteSMPServer(userServers, operatorIndex, serverIndex)
} else {
deleteXFTPServer(userServers, operatorIndex, serverIndex)
when (protocol) {
ServerProtocol.NTF -> deleteNTFServer(userServers, operatorIndex, serverIndex)
ServerProtocol.SMP -> deleteSMPServer(userServers, operatorIndex, serverIndex)
ServerProtocol.XFTP -> deleteXFTPServer(userServers, operatorIndex, serverIndex)
}
close()
},
onUpdate = { updatedServer ->
userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
smpServers = if (protocol == ServerProtocol.SMP) {
this[operatorIndex].smpServers.toMutableList().apply {
this[serverIndex] = updatedServer
}
} else this[operatorIndex].smpServers,
xftpServers = if (protocol == ServerProtocol.XFTP) {
this[operatorIndex].xftpServers.toMutableList().apply {
this[serverIndex] = updatedServer
}
} else this[operatorIndex].xftpServers
)
if (platform.supportsPushNotifications && protocol == ServerProtocol.NTF && updatedServer.enabled) {
// We keep a single ntf server, if the updatedServer is enabled, we disable all other ntf servers first
this.replaceAll { op ->
op.copy(ntfServers = op.ntfServers.map { server -> server.copy(enabled = false).also { s -> Log.d(TAG, "ntf: $s")} })
}
}
this[operatorIndex] = when (protocol) {
ServerProtocol.NTF -> this[operatorIndex].copy(ntfServers = this[operatorIndex].ntfServers.toMutableList().apply { this[serverIndex] = updatedServer })
ServerProtocol.SMP -> this[operatorIndex].copy(smpServers = this[operatorIndex].smpServers.toMutableList().apply { this[serverIndex] = updatedServer })
ServerProtocol.XFTP -> this[operatorIndex].copy(xftpServers = this[operatorIndex].xftpServers.toMutableList().apply { this[serverIndex] = updatedServer })
}
}
},
close = close,
@@ -115,7 +115,11 @@ private fun ProtocolServerLayout(
onDelete: () -> Unit,
) {
ColumnWithScrollBar {
AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.XFTP) MR.strings.xftp_server else MR.strings.smp_server))
AppBarTitle(stringResource(when (serverProtocol) {
ServerProtocol.NTF -> MR.strings.ntf_server
ServerProtocol.XFTP -> MR.strings.xftp_server
ServerProtocol.SMP -> MR.strings.smp_server
}))
if (server.value.preset) {
PresetServer(server, testing, testServer)
@@ -389,6 +389,32 @@ private suspend fun runServersTest(servers: List<UserServer>, m: ChatModel, onUp
return fs
}
fun deleteNTFServer(
userServers: MutableState<List<UserOperatorServers>>,
operatorServersIndex: Int,
serverIndex: Int
) {
val serverIsSaved = userServers.value[operatorServersIndex].ntfServers[serverIndex].serverId != null
if (serverIsSaved) {
userServers.value = userServers.value.toMutableList().apply {
this[operatorServersIndex] = this[operatorServersIndex].copy(
ntfServers = this[operatorServersIndex].ntfServers.toMutableList().apply {
this[serverIndex] = this[serverIndex].copy(deleted = true)
}
)
}
} else {
userServers.value = userServers.value.toMutableList().apply {
this[operatorServersIndex] = this[operatorServersIndex].copy(
ntfServers = this[operatorServersIndex].ntfServers.toMutableList().apply {
this.removeAt(serverIndex)
}
)
}
}
}
fun deleteXFTPServer(
userServers: MutableState<List<UserOperatorServers>>,
operatorServersIndex: Int,
@@ -2642,6 +2642,7 @@
<string name="servers_info_starting_from">Starting from %s.</string>
<string name="smp_server">SMP server</string>
<string name="xftp_server">XFTP server</string>
<string name="ntf_server">NTF server</string>
<string name="reconnect">Reconnect</string>
<string name="attempts_label">attempts</string>
<string name="sent_directly">Sent directly</string>
+7 -2
View File
@@ -11,8 +11,13 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: d352d518c2b3a42bc7a298954dde799422e1457f
location: https://codeberg.org/s1m/sxmq.git
tag: f5720a254104d70b33ac1479ffa9d24ba9988b59
-- source-repository-package
-- type: git
-- location: https://github.com/simplex-chat/simplexmq.git
-- tag: d352d518c2b3a42bc7a298954dde799422e1457f
source-repository-package
type: git
+1
View File
@@ -1,4 +1,5 @@
{
"https://codeberg.org/s1m/sxmq.git"."f5720a254104d70b33ac1479ffa9d24ba9988b59" = "cf9a74de0d05afa60a74aa8aa54205c718286b9c16c5f0f398a24d7fa7f7b1ff";
"https://github.com/simplex-chat/simplexmq.git"."d352d518c2b3a42bc7a298954dde799422e1457f" = "1rha84pfpaqx3mf218szkfra334vhijqf17hanxqmp1sicfbf1x3";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";