mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-25 16:24:27 +00:00
Add support for Push notifications
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+11
-2
@@ -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()
|
||||
|
||||
+15
-16
@@ -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,
|
||||
|
||||
+5
-1
@@ -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)
|
||||
|
||||
+26
@@ -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
@@ -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,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";
|
||||
|
||||
Reference in New Issue
Block a user