mirror of
https://github.com/nextcloud/talk-android
synced 2025-07-08 13:29:49 +01:00
Beginning to work on new push notifications setup
Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
parent
59c9106ff8
commit
d75e84c1fa
@ -19,6 +19,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "androidx.work:work-gcm:2.3.0-beta02"
|
implementation "androidx.work:work-gcm:2.3.1"
|
||||||
implementation "com.google.firebase:firebase-messaging:20.1.0"
|
implementation "com.google.firebase:firebase-messaging:20.1.0"
|
||||||
}
|
}
|
||||||
|
@ -20,19 +20,71 @@
|
|||||||
package com.nextcloud.talk.services.firebase
|
package com.nextcloud.talk.services.firebase
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService
|
import android.app.Notification
|
||||||
import com.google.firebase.messaging.RemoteMessage
|
import android.app.PendingIntent
|
||||||
import com.nextcloud.talk.jobs.NotificationWorker
|
import android.content.Intent
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
import android.media.AudioAttributes
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.emoji.text.EmojiCompat
|
||||||
import androidx.work.Data
|
import androidx.work.Data
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
import com.bluelinelabs.logansquare.LoganSquare
|
||||||
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
|
import com.nextcloud.talk.R
|
||||||
|
import com.nextcloud.talk.activities.MagicCallActivity
|
||||||
|
import com.nextcloud.talk.jobs.MessageNotificationWorker
|
||||||
|
import com.nextcloud.talk.jobs.NotificationWorker
|
||||||
|
import com.nextcloud.talk.models.SignatureVerification
|
||||||
|
import com.nextcloud.talk.models.json.push.DecryptedPushMessage
|
||||||
|
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
|
||||||
|
import com.nextcloud.talk.utils.NotificationUtils
|
||||||
|
import com.nextcloud.talk.utils.NotificationUtils.cancelAllNotificationsForAccount
|
||||||
|
import com.nextcloud.talk.utils.NotificationUtils.cancelExistingNotificationWithId
|
||||||
|
import com.nextcloud.talk.utils.PushUtils
|
||||||
|
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||||
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL
|
||||||
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
|
||||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||||
|
import okhttp3.JavaNetCookieJar
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.KoinComponent
|
||||||
import org.koin.core.inject
|
import org.koin.core.inject
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import java.net.CookieManager
|
||||||
|
import java.security.InvalidKeyException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.util.*
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.NoSuchPaddingException
|
||||||
|
|
||||||
class MagicFirebaseMessagingService : FirebaseMessagingService(), KoinComponent {
|
class MagicFirebaseMessagingService : FirebaseMessagingService(), KoinComponent {
|
||||||
val appPreferences: AppPreferences by inject()
|
val appPreferences: AppPreferences by inject()
|
||||||
|
val retrofit: Retrofit by inject()
|
||||||
|
val okHttpClient: OkHttpClient by inject()
|
||||||
|
val eventBus: EventBus by inject()
|
||||||
|
val usersRepository: UsersRepository by inject()
|
||||||
|
|
||||||
|
private var isServiceInForeground: Boolean = false
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
eventBus.register(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
eventBus.unregister(this)
|
||||||
|
isServiceInForeground = false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNewToken(token: String) {
|
override fun onNewToken(token: String) {
|
||||||
super.onNewToken(token)
|
super.onNewToken(token)
|
||||||
@ -42,15 +94,143 @@ class MagicFirebaseMessagingService : FirebaseMessagingService(), KoinComponent
|
|||||||
@SuppressLint("LongLogTag")
|
@SuppressLint("LongLogTag")
|
||||||
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
||||||
remoteMessage.data.let {
|
remoteMessage.data.let {
|
||||||
val messageData: Data = Data.Builder()
|
decryptMessage(it["subject"]!!, it["signature"]!!)
|
||||||
.putString(BundleKeys.KEY_NOTIFICATION_SUBJECT, it["subject"])
|
|
||||||
.putString(BundleKeys.KEY_NOTIFICATION_SIGNATURE, it["signature"])
|
|
||||||
.build()
|
|
||||||
val pushNotificationWork: OneTimeWorkRequest = OneTimeWorkRequest.Builder(NotificationWorker::class.java)
|
|
||||||
.setInputData(messageData)
|
|
||||||
.build()
|
|
||||||
WorkManager.getInstance().enqueue(pushNotificationWork)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun decryptMessage(subject: String, signature: String) {
|
||||||
|
val signatureVerification: SignatureVerification
|
||||||
|
val decryptedPushMessage: DecryptedPushMessage
|
||||||
|
|
||||||
|
try {
|
||||||
|
val base64DecodedSubject = Base64.decode(subject, Base64.DEFAULT)
|
||||||
|
val base64DecodedSignature = Base64.decode(signature, Base64.DEFAULT)
|
||||||
|
val pushUtils = PushUtils(usersRepository)
|
||||||
|
val privateKey = pushUtils.readKeyFromFile(false) as PrivateKey
|
||||||
|
try {
|
||||||
|
signatureVerification = pushUtils.verifySignature(base64DecodedSignature, base64DecodedSubject)
|
||||||
|
if (signatureVerification.signatureValid) {
|
||||||
|
val cipher = Cipher.getInstance("RSA/None/PKCS1Padding")
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, privateKey)
|
||||||
|
val decryptedSubject = cipher.doFinal(base64DecodedSubject)
|
||||||
|
decryptedPushMessage = LoganSquare.parse(String(decryptedSubject), DecryptedPushMessage::class.java)
|
||||||
|
decryptedPushMessage.apply {
|
||||||
|
val timestamp = decryptedPushMessage.timestamp
|
||||||
|
when {
|
||||||
|
delete -> {
|
||||||
|
cancelExistingNotificationWithId(applicationContext, signatureVerification.userEntity, notificationId!!)
|
||||||
|
}
|
||||||
|
deleteAll -> {
|
||||||
|
cancelAllNotificationsForAccount(applicationContext, signatureVerification.userEntity)
|
||||||
|
}
|
||||||
|
type == "call" -> {
|
||||||
|
val fullScreenIntent = Intent(applicationContext, MagicCallActivity::class.java)
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putString(BundleKeys.KEY_ROOM_ID, decryptedPushMessage.id)
|
||||||
|
bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.userEntity)
|
||||||
|
bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, true)
|
||||||
|
fullScreenIntent.putExtras(bundle)
|
||||||
|
|
||||||
|
fullScreenIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
val fullScreenPendingIntent = PendingIntent.getActivity(this@MagicFirebaseMessagingService, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
|
||||||
|
val audioAttributesBuilder = AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
|
audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST)
|
||||||
|
|
||||||
|
val soundUri = NotificationUtils.getCallSoundUri(applicationContext, appPreferences)
|
||||||
|
val vibrationEffect = NotificationUtils.getVibrationEffect(appPreferences)
|
||||||
|
|
||||||
|
val notificationChannelId = NotificationUtils.getNotificationChannelId(applicationContext, applicationContext.resources
|
||||||
|
.getString(R.string.nc_notification_channel_calls), applicationContext.resources
|
||||||
|
.getString(R.string.nc_notification_channel_calls_description), true,
|
||||||
|
NotificationManagerCompat.IMPORTANCE_HIGH, soundUri!!,
|
||||||
|
audioAttributesBuilder.build(), vibrationEffect, false, null)
|
||||||
|
|
||||||
|
val userBaseUrl = Uri.parse(signatureVerification.userEntity.baseUrl).toString()
|
||||||
|
|
||||||
|
val notificationBuilder = NotificationCompat.Builder(this@MagicFirebaseMessagingService, notificationChannelId)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||||
|
.setSmallIcon(R.drawable.ic_call_black_24dp)
|
||||||
|
.setSubText(userBaseUrl)
|
||||||
|
.setShowWhen(true)
|
||||||
|
.setWhen(decryptedPushMessage.timestamp)
|
||||||
|
.setContentTitle(EmojiCompat.get().process(decryptedPushMessage.subject.toString()))
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setOngoing(true)
|
||||||
|
//.setTimeoutAfter(45000L)
|
||||||
|
.setFullScreenIntent(fullScreenPendingIntent, true)
|
||||||
|
.setSound(NotificationUtils.getCallSoundUri(applicationContext, appPreferences))
|
||||||
|
|
||||||
|
if (vibrationEffect != null) {
|
||||||
|
notificationBuilder.setVibrate(vibrationEffect)
|
||||||
|
}
|
||||||
|
|
||||||
|
val notification = notificationBuilder.build()
|
||||||
|
notification.flags = notification.flags or Notification.FLAG_INSISTENT
|
||||||
|
isServiceInForeground = true
|
||||||
|
checkIfCallIsActive(signatureVerification, decryptedPushMessage)
|
||||||
|
startForeground(decryptedPushMessage.timestamp.toInt(), notification)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val messageData = Data.Builder()
|
||||||
|
.putString(BundleKeys.KEY_DECRYPTED_PUSH_MESSAGE, LoganSquare.serialize(decryptedPushMessage))
|
||||||
|
.putString(BundleKeys.KEY_SIGNATURE_VERIFICATION, LoganSquare.serialize(signatureVerification))
|
||||||
|
.build()
|
||||||
|
val pushNotificationWork = OneTimeWorkRequest.Builder(MessageNotificationWorker::class.java).setInputData(messageData).build()
|
||||||
|
WorkManager.getInstance().enqueue(pushNotificationWork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e1: NoSuchAlgorithmException) {
|
||||||
|
Log.d(NotificationWorker.TAG, "No proper algorithm to decrypt the message " + e1.localizedMessage)
|
||||||
|
} catch (e1: NoSuchPaddingException) {
|
||||||
|
Log.d(NotificationWorker.TAG, "No proper padding to decrypt the message " + e1.localizedMessage)
|
||||||
|
} catch (e1: InvalidKeyException) {
|
||||||
|
Log.d(NotificationWorker.TAG, "Invalid private key " + e1.localizedMessage)
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.d(NotificationWorker.TAG, "Something went very wrong " + exception.localizedMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkIfCallIsActive(signatureVerification: SignatureVerification, decryptedPushMessage: DecryptedPushMessage) {
|
||||||
|
/*val ncApi = retrofit.newBuilder().client(okHttpClient.newBuilder().cookieJar(JavaNetCookieJar(CookieManager())).build()).build().create(NcApi::class.java)
|
||||||
|
var hasParticipantsInCall = false
|
||||||
|
var inCallOnDifferentDevice = false
|
||||||
|
|
||||||
|
ncApi.getPeersForCall(ApiUtils.getCredentials(signatureVerification.userEntity.username, signatureVerification.userEntity.token),
|
||||||
|
ApiUtils.getUrlForParticipants(signatureVerification.userEntity.baseUrl,
|
||||||
|
decryptedPushMessage.id))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe(object : Observer<ParticipantsOverall> {
|
||||||
|
override fun onSubscribe(d: Disposable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNext(participantsOverall: ParticipantsOverall) {
|
||||||
|
val participantList: List<Participant> = participantsOverall.ocs.data
|
||||||
|
for (participant in participantList) {
|
||||||
|
if (participant.participantFlags != Participant.ParticipantFlags.NOT_IN_CALL) {
|
||||||
|
hasParticipantsInCall = true
|
||||||
|
if (participant.userId == signatureVerification.userEntity.userId) {
|
||||||
|
inCallOnDifferentDevice = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasParticipantsInCall || inCallOnDifferentDevice) {
|
||||||
|
stopForeground(true)
|
||||||
|
} else if (isServiceInForeground) {
|
||||||
|
checkIfCallIsActive(signatureVerification, decryptedPushMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(e: Throwable) {}
|
||||||
|
override fun onComplete() {
|
||||||
|
}
|
||||||
|
})*/
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,254 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk application
|
||||||
|
*
|
||||||
|
* @author Mario Danic
|
||||||
|
* Copyright (C) 2020 Mario Danic <mario@lovelyhq.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.nextcloud.talk.jobs
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.media.AudioAttributes
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.app.Person
|
||||||
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
|
import androidx.emoji.text.EmojiCompat
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import coil.Coil
|
||||||
|
import coil.target.Target
|
||||||
|
import coil.transform.CircleCropTransformation
|
||||||
|
import com.bluelinelabs.logansquare.LoganSquare
|
||||||
|
import com.nextcloud.talk.R
|
||||||
|
import com.nextcloud.talk.activities.MainActivity
|
||||||
|
import com.nextcloud.talk.models.SignatureVerification
|
||||||
|
import com.nextcloud.talk.models.json.conversations.Conversation
|
||||||
|
import com.nextcloud.talk.models.json.push.DecryptedPushMessage
|
||||||
|
import com.nextcloud.talk.newarch.utils.Images
|
||||||
|
import com.nextcloud.talk.utils.ApiUtils
|
||||||
|
import com.nextcloud.talk.utils.NotificationUtils
|
||||||
|
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||||
|
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||||
|
import org.koin.core.KoinComponent
|
||||||
|
import org.koin.core.inject
|
||||||
|
import java.util.function.Consumer
|
||||||
|
|
||||||
|
class MessageNotificationWorker(
|
||||||
|
context: Context,
|
||||||
|
workerParams: WorkerParameters
|
||||||
|
) : CoroutineWorker(context, workerParams), KoinComponent {
|
||||||
|
val appPreferences: AppPreferences by inject()
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
val data = inputData
|
||||||
|
val decryptedPushMessageString: String = data.getString(BundleKeys.KEY_DECRYPTED_PUSH_MESSAGE)!!
|
||||||
|
val signatureVerificationString: String = data.getString(BundleKeys.KEY_SIGNATURE_VERIFICATION)!!
|
||||||
|
|
||||||
|
val decryptedPushMessage = LoganSquare.parse(decryptedPushMessageString, DecryptedPushMessage::class.java)
|
||||||
|
val signatureVerification = LoganSquare.parse(signatureVerificationString, SignatureVerification::class.java)
|
||||||
|
|
||||||
|
// we support Nextcloud Talk 4.0 and up so assuming "no-ping" capability here
|
||||||
|
val intent = Intent(applicationContext, MainActivity::class.java)
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, signatureVerification.userEntity)
|
||||||
|
bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, decryptedPushMessage.id)
|
||||||
|
intent.putExtras(bundle)
|
||||||
|
|
||||||
|
when (decryptedPushMessage.type) {
|
||||||
|
"room" -> {
|
||||||
|
showNotificationWithObjectData(decryptedPushMessage, signatureVerification, intent)
|
||||||
|
}
|
||||||
|
"chat" -> {
|
||||||
|
if (decryptedPushMessage.notificationId != null) {
|
||||||
|
showNotificationWithObjectData(decryptedPushMessage, signatureVerification, intent)
|
||||||
|
} else {
|
||||||
|
showNotification(decryptedPushMessage, signatureVerification, null, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNotificationWithObjectData(decryptedPushMessage: DecryptedPushMessage, signatureVerification: SignatureVerification, intent: Intent) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNotification(
|
||||||
|
decryptedPushMessage: DecryptedPushMessage,
|
||||||
|
signatureVerification: SignatureVerification,
|
||||||
|
conversationType: Conversation.ConversationType?, intent: Intent) {
|
||||||
|
val largeIcon = when (conversationType) {
|
||||||
|
Conversation.ConversationType.PUBLIC_CONVERSATION -> {
|
||||||
|
BitmapFactory.decodeResource(applicationContext.resources, R.drawable.ic_link_black_24px)
|
||||||
|
}
|
||||||
|
Conversation.ConversationType.GROUP_CONVERSATION -> {
|
||||||
|
BitmapFactory.decodeResource(applicationContext.resources, R.drawable.ic_people_group_black_24px)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// one to one and unknown
|
||||||
|
BitmapFactory.decodeResource(applicationContext.resources, R.drawable.ic_chat_black_24dp)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val adjustedConversationType = conversationType ?: Conversation.ConversationType.ONE_TO_ONE_CONVERSATION
|
||||||
|
|
||||||
|
val pendingIntent: PendingIntent? = PendingIntent.getActivity(applicationContext,
|
||||||
|
0, intent, 0)
|
||||||
|
|
||||||
|
val userBaseUrl = Uri.parse(signatureVerification.userEntity.baseUrl).toString()
|
||||||
|
val soundUri = NotificationUtils.getMessageSoundUri(applicationContext, appPreferences)
|
||||||
|
|
||||||
|
val audioAttributesBuilder: AudioAttributes.Builder =
|
||||||
|
AudioAttributes.Builder()
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
|
.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
|
||||||
|
|
||||||
|
val vibrationEffect = NotificationUtils.getVibrationEffect(appPreferences)
|
||||||
|
|
||||||
|
val notificationChannelId = NotificationUtils.getNotificationChannelId(applicationContext, applicationContext.resources
|
||||||
|
.getString(R.string.nc_notification_channel_messages), applicationContext.resources
|
||||||
|
.getString(R.string.nc_notification_channel_messages), true,
|
||||||
|
NotificationManagerCompat.IMPORTANCE_HIGH, soundUri!!,
|
||||||
|
audioAttributesBuilder.build(), vibrationEffect, false, null)
|
||||||
|
|
||||||
|
val notificationBuilder = NotificationCompat.Builder(applicationContext, notificationChannelId)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setCategory(Notification.CATEGORY_MESSAGE)
|
||||||
|
.setSmallIcon(R.drawable.ic_logo)
|
||||||
|
.setLargeIcon(largeIcon)
|
||||||
|
.setSubText(userBaseUrl)
|
||||||
|
.setShowWhen(true)
|
||||||
|
.setWhen(decryptedPushMessage.timestamp)
|
||||||
|
.setContentTitle(EmojiCompat.get().process(decryptedPushMessage.subject.toString()))
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setSound(soundUri)
|
||||||
|
|
||||||
|
if (vibrationEffect != null) {
|
||||||
|
notificationBuilder.setVibrate(vibrationEffect)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 23) {
|
||||||
|
// This method should exist since API 21, but some phones don't have it
|
||||||
|
// So as a safeguard, we don't use it until 23
|
||||||
|
notificationBuilder.color = applicationContext.resources.getColor(R.color.colorPrimary)
|
||||||
|
}
|
||||||
|
|
||||||
|
var notificationId = decryptedPushMessage.timestamp.toInt()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && decryptedPushMessage.notificationUser != null && decryptedPushMessage.type == "chat") {
|
||||||
|
var style: NotificationCompat.MessagingStyle? = null
|
||||||
|
|
||||||
|
decryptedPushMessage.id?.let { decryptedMessageId ->
|
||||||
|
val activeStatusBarNotification =
|
||||||
|
NotificationUtils.findNotificationForRoom(
|
||||||
|
applicationContext,
|
||||||
|
signatureVerification.userEntity, decryptedMessageId)
|
||||||
|
activeStatusBarNotification?.let { activeStatusBarNotification ->
|
||||||
|
notificationId = activeStatusBarNotification.id
|
||||||
|
style = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(activeStatusBarNotification.notification)
|
||||||
|
notificationBuilder.setStyle(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationBuilder.setOnlyAlertOnce(true)
|
||||||
|
decryptedPushMessage.notificationUser?.let { notificationUser ->
|
||||||
|
val person = Person.Builder().setKey(signatureVerification.userEntity.id.toString() + "@" + notificationUser.id)
|
||||||
|
.setName(EmojiCompat.get().process(notificationUser.name))
|
||||||
|
.setBot(notificationUser.type == "bot")
|
||||||
|
|
||||||
|
if (notificationUser.type == "user" || notificationUser.type == "guest") {
|
||||||
|
val avatarUrl = when (notificationUser.type) {
|
||||||
|
"user" -> {
|
||||||
|
ApiUtils.getUrlForAvatarWithName(signatureVerification.userEntity.baseUrl, notificationUser.id, R.dimen.avatar_size)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
ApiUtils.getUrlForAvatarWithNameForGuests(signatureVerification.userEntity.baseUrl, notificationUser.name, R.dimen.avatar_size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val target = object : Target {
|
||||||
|
override fun onSuccess(result: Drawable) {
|
||||||
|
super.onSuccess(result)
|
||||||
|
person.setIcon(IconCompat.createWithBitmap(result.toBitmap()))
|
||||||
|
notificationBuilder.setStyle(getStyle(decryptedPushMessage, adjustedConversationType, person.build(), style))
|
||||||
|
NotificationManagerCompat.from(applicationContext).notify(notificationId, notificationBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(error: Drawable?) {
|
||||||
|
super.onError(error)
|
||||||
|
notificationBuilder.setStyle(getStyle(decryptedPushMessage, adjustedConversationType, person.build(), style))
|
||||||
|
NotificationManagerCompat.from(applicationContext).notify(notificationId, notificationBuilder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val request = Images().getRequestForUrl(
|
||||||
|
Coil.loader(), applicationContext, avatarUrl!!, signatureVerification.userEntity,
|
||||||
|
target, null, CircleCropTransformation()
|
||||||
|
)
|
||||||
|
|
||||||
|
Coil.loader().load(request)
|
||||||
|
} else {
|
||||||
|
notificationBuilder.setStyle(getStyle(decryptedPushMessage, adjustedConversationType, person.build(), style))
|
||||||
|
NotificationManagerCompat.from(applicationContext).notify(notificationId, notificationBuilder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NotificationManagerCompat.from(applicationContext).notify(notificationId, notificationBuilder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun getStyle(
|
||||||
|
decryptedPushMessage: DecryptedPushMessage,
|
||||||
|
conversationType: Conversation.ConversationType,
|
||||||
|
person: Person,
|
||||||
|
style: NotificationCompat.MessagingStyle?
|
||||||
|
): NotificationCompat.MessagingStyle? {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
val newStyle = NotificationCompat.MessagingStyle(person)
|
||||||
|
newStyle.conversationTitle = decryptedPushMessage.subject
|
||||||
|
newStyle.isGroupConversation = conversationType != Conversation.ConversationType.ONE_TO_ONE_CONVERSATION
|
||||||
|
style?.messages?.forEach(
|
||||||
|
Consumer { message: NotificationCompat.MessagingStyle.Message ->
|
||||||
|
newStyle.addMessage(
|
||||||
|
NotificationCompat.MessagingStyle.Message(
|
||||||
|
message.text,
|
||||||
|
message.timestamp, message.person
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
newStyle.addMessage(decryptedPushMessage.text, decryptedPushMessage.timestamp, person)
|
||||||
|
return newStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
// we'll never come here
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -426,8 +426,7 @@ class NotificationWorker(
|
|||||||
signatureVerification!!.userEntity, decryptedPushMessage!!.id
|
signatureVerification!!.userEntity, decryptedPushMessage!!.id
|
||||||
)
|
)
|
||||||
val notificationId: Int
|
val notificationId: Int
|
||||||
notificationId = activeStatusBarNotification?.id ?: crc32.value
|
notificationId = activeStatusBarNotification?.id ?: crc32.value.toInt()
|
||||||
.toInt()
|
|
||||||
if (VERSION.SDK_INT >= VERSION_CODES.N && decryptedPushMessage!!.notificationUser != null && decryptedPushMessage!!.type == "chat") {
|
if (VERSION.SDK_INT >= VERSION_CODES.N && decryptedPushMessage!!.notificationUser != null && decryptedPushMessage!!.type == "chat") {
|
||||||
var style: MessagingStyle? = null
|
var style: MessagingStyle? = null
|
||||||
if (activeStatusBarNotification != null) {
|
if (activeStatusBarNotification != null) {
|
||||||
@ -482,7 +481,6 @@ class NotificationWorker(
|
|||||||
notificationBuilder.setStyle(getStyle(person.build(), style))
|
notificationBuilder.setStyle(getStyle(person.build(), style))
|
||||||
sendNotificationWithId(notificationId, notificationBuilder.build())
|
sendNotificationWithId(notificationId, notificationBuilder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val request = Images().getRequestForUrl(
|
val request = Images().getRequestForUrl(
|
||||||
@ -501,36 +499,6 @@ class NotificationWorker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getStyle(
|
|
||||||
person: Person,
|
|
||||||
style: MessagingStyle?
|
|
||||||
): MessagingStyle? {
|
|
||||||
if (VERSION.SDK_INT >= VERSION_CODES.N) {
|
|
||||||
val newStyle =
|
|
||||||
MessagingStyle(person)
|
|
||||||
newStyle.conversationTitle = decryptedPushMessage!!.subject
|
|
||||||
newStyle.isGroupConversation = conversationType != "one2one"
|
|
||||||
style?.messages?.forEach(
|
|
||||||
Consumer { message: Message ->
|
|
||||||
newStyle.addMessage(
|
|
||||||
Message(
|
|
||||||
message.text,
|
|
||||||
message.timestamp, message.person
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
newStyle.addMessage(
|
|
||||||
decryptedPushMessage!!.text, decryptedPushMessage!!.timestamp,
|
|
||||||
person
|
|
||||||
)
|
|
||||||
return newStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
// we'll never come here
|
|
||||||
|
|
||||||
return style
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendNotificationWithId(
|
private fun sendNotificationWithId(
|
||||||
notificationId: Int,
|
notificationId: Int,
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
/*
|
|
||||||
* Nextcloud Talk application
|
|
||||||
*
|
|
||||||
* @author Mario Danic
|
|
||||||
* Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.nextcloud.talk.models.json.push;
|
|
||||||
|
|
||||||
import com.bluelinelabs.logansquare.annotation.JsonField;
|
|
||||||
import com.bluelinelabs.logansquare.annotation.JsonIgnore;
|
|
||||||
import com.bluelinelabs.logansquare.annotation.JsonObject;
|
|
||||||
|
|
||||||
import org.parceler.Parcel;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Parcel
|
|
||||||
@JsonObject
|
|
||||||
public class DecryptedPushMessage {
|
|
||||||
@JsonField(name = "app")
|
|
||||||
public String app;
|
|
||||||
|
|
||||||
@JsonField(name = "type")
|
|
||||||
public String type;
|
|
||||||
|
|
||||||
@JsonField(name = "subject")
|
|
||||||
public String subject;
|
|
||||||
|
|
||||||
@JsonField(name = "id")
|
|
||||||
public String id;
|
|
||||||
|
|
||||||
@JsonField(name = "nid")
|
|
||||||
public long notificationId;
|
|
||||||
|
|
||||||
@JsonField(name = "delete")
|
|
||||||
public boolean delete;
|
|
||||||
|
|
||||||
@JsonField(name = "delete-all")
|
|
||||||
public boolean deleteAll;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public NotificationUser notificationUser;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public String text;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
public long timestamp;
|
|
||||||
}
|
|
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk application
|
||||||
|
*
|
||||||
|
* @author Mario Danic
|
||||||
|
* Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.nextcloud.talk.models.json.push
|
||||||
|
|
||||||
|
import com.bluelinelabs.logansquare.annotation.JsonField
|
||||||
|
import com.bluelinelabs.logansquare.annotation.JsonIgnore
|
||||||
|
import com.bluelinelabs.logansquare.annotation.JsonObject
|
||||||
|
import lombok.Data
|
||||||
|
import org.parceler.Parcel
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Parcel
|
||||||
|
@JsonObject
|
||||||
|
class DecryptedPushMessage {
|
||||||
|
@JvmField
|
||||||
|
@JsonField(name = ["app"])
|
||||||
|
var app: String? = null
|
||||||
|
@JvmField
|
||||||
|
@JsonField(name = ["type"])
|
||||||
|
var type: String? = null
|
||||||
|
@JvmField
|
||||||
|
@JsonField(name = ["subject"])
|
||||||
|
var subject: String? = null
|
||||||
|
@JvmField
|
||||||
|
@JsonField(name = ["id"])
|
||||||
|
var id: String? = null
|
||||||
|
@JvmField
|
||||||
|
@JsonField(name = ["nid"])
|
||||||
|
var notificationId: Long? = null
|
||||||
|
@JvmField
|
||||||
|
@JsonField(name = ["delete"])
|
||||||
|
var delete = false
|
||||||
|
@JvmField
|
||||||
|
@JsonField(name = ["delete-all"])
|
||||||
|
var deleteAll = false
|
||||||
|
@JvmField
|
||||||
|
@JsonIgnore
|
||||||
|
var notificationUser: NotificationUser? = null
|
||||||
|
@JvmField
|
||||||
|
@JsonIgnore
|
||||||
|
var text: String? = null
|
||||||
|
@JvmField
|
||||||
|
@JsonIgnore
|
||||||
|
var timestamp: Long = 0
|
||||||
|
}
|
@ -26,11 +26,21 @@ import android.app.NotificationChannel
|
|||||||
import android.app.NotificationChannelGroup
|
import android.app.NotificationChannelGroup
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.media.AudioAttributes
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.service.notification.StatusBarNotification
|
import android.service.notification.StatusBarNotification
|
||||||
|
import android.text.TextUtils
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import com.bluelinelabs.logansquare.LoganSquare
|
||||||
import com.nextcloud.talk.R
|
import com.nextcloud.talk.R
|
||||||
|
import com.nextcloud.talk.models.RingtoneSettings
|
||||||
import com.nextcloud.talk.newarch.local.models.UserNgEntity
|
import com.nextcloud.talk.newarch.local.models.UserNgEntity
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||||
|
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
object NotificationUtils {
|
object NotificationUtils {
|
||||||
val NOTIFICATION_CHANNEL_CALLS = "NOTIFICATION_CHANNEL_CALLS"
|
val NOTIFICATION_CHANNEL_CALLS = "NOTIFICATION_CHANNEL_CALLS"
|
||||||
@ -40,38 +50,87 @@ object NotificationUtils {
|
|||||||
val NOTIFICATION_CHANNEL_MESSAGES_V3 = "NOTIFICATION_CHANNEL_MESSAGES_V3"
|
val NOTIFICATION_CHANNEL_MESSAGES_V3 = "NOTIFICATION_CHANNEL_MESSAGES_V3"
|
||||||
val NOTIFICATION_CHANNEL_CALLS_V3 = "NOTIFICATION_CHANNEL_CALLS_V3"
|
val NOTIFICATION_CHANNEL_CALLS_V3 = "NOTIFICATION_CHANNEL_CALLS_V3"
|
||||||
|
|
||||||
|
fun getVibrationEffect(appPreferences: AppPreferences): LongArray? {
|
||||||
|
return if (appPreferences.shouldVibrateSetting) {
|
||||||
|
longArrayOf(0L, 400L, 800L, 600L, 800L, 800L, 800L, 1000L)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCallSoundUri(context: Context, appPreferences: AppPreferences) : Uri? {
|
||||||
|
val ringtonePreferencesString: String? = appPreferences.callRingtoneUri
|
||||||
|
|
||||||
|
return if (TextUtils.isEmpty(ringtonePreferencesString)) {
|
||||||
|
Uri.parse("android.resource://" + context.packageName +
|
||||||
|
"/raw/librem_by_feandesign_call")
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
val ringtoneSettings = LoganSquare.parse(ringtonePreferencesString, RingtoneSettings::class.java)
|
||||||
|
ringtoneSettings.ringtoneUri
|
||||||
|
} catch (exception: IOException) {
|
||||||
|
Uri.parse("android.resource://" + context.packageName + "/raw/librem_by_feandesign_call")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMessageSoundUri(context: Context, appPreferences: AppPreferences) : Uri? {
|
||||||
|
val ringtonePreferencesString: String? = appPreferences.messageRingtoneUri
|
||||||
|
|
||||||
|
return if (TextUtils.isEmpty(ringtonePreferencesString)) {
|
||||||
|
Uri.parse("android.resource://" + context.packageName + "/raw/librem_by_feandesign_message")
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
val ringtoneSettings = LoganSquare.parse(ringtonePreferencesString, RingtoneSettings::class.java)
|
||||||
|
ringtoneSettings.ringtoneUri
|
||||||
|
} catch (exception: IOException) {
|
||||||
|
Uri.parse("android.resource://" + context.packageName + "/raw/librem_by_feandesign_message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNotificationChannelId(context: Context, channelName: String,
|
||||||
|
channelDescription: String, enableLights: Boolean,
|
||||||
|
importance: Int, sound: Uri, audioAttributes: AudioAttributes, vibrationPattern: LongArray?, bypassDnd: Boolean, lockScreenVisibility: Integer?): String {
|
||||||
|
val channelId = Objects.hash(channelName, channelDescription, enableLights, importance, sound, audioAttributes, vibrationPattern, bypassDnd, lockScreenVisibility).toString()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
|
||||||
|
createNotificationChannel(context, channelId, channelName, channelDescription, enableLights, importance, sound, audioAttributes, vibrationPattern, bypassDnd, lockScreenVisibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
return channelId
|
||||||
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.O)
|
@TargetApi(Build.VERSION_CODES.O)
|
||||||
fun createNotificationChannel(
|
fun createNotificationChannel(context: Context,
|
||||||
context: Context,
|
channelId: String, channelName: String,
|
||||||
channelId: String,
|
channelDescription: String, enableLights: Boolean,
|
||||||
channelName: String,
|
importance: Int, sound: Uri, audioAttributes: AudioAttributes,
|
||||||
channelDescription: String,
|
vibrationPattern: LongArray?, bypassDnd: Boolean = false, lockScreenVisibility: Integer?) {
|
||||||
enableLights: Boolean,
|
|
||||||
importance: Int
|
|
||||||
) {
|
|
||||||
|
|
||||||
val notificationManager =
|
val notificationManagerCompat = NotificationManagerCompat.from(context)
|
||||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
if (notificationManagerCompat.getNotificationChannel(channelId) == null) {
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O && notificationManager.getNotificationChannel(
|
val channel = NotificationChannel(channelId, channelName, importance)
|
||||||
channelId
|
|
||||||
) == null
|
|
||||||
) {
|
|
||||||
|
|
||||||
val channel = NotificationChannel(
|
|
||||||
channelId, channelName,
|
|
||||||
importance
|
|
||||||
)
|
|
||||||
|
|
||||||
channel.description = channelDescription
|
channel.description = channelDescription
|
||||||
channel.enableLights(enableLights)
|
channel.enableLights(enableLights)
|
||||||
channel.lightColor = R.color.colorPrimary
|
channel.lightColor = R.color.colorPrimary
|
||||||
channel.setSound(null, null)
|
channel.setSound(sound, audioAttributes)
|
||||||
|
if (vibrationPattern != null) {
|
||||||
|
channel.enableVibration(true)
|
||||||
|
channel.vibrationPattern = vibrationPattern
|
||||||
|
} else {
|
||||||
|
channel.enableVibration(false)
|
||||||
|
}
|
||||||
|
channel.setBypassDnd(bypassDnd)
|
||||||
|
if (lockScreenVisibility != null) {
|
||||||
|
channel.lockscreenVisibility = lockScreenVisibility
|
||||||
|
}
|
||||||
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
notificationManagerCompat.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.O)
|
@TargetApi(Build.VERSION_CODES.O)
|
||||||
fun createNotificationChannelGroup(
|
fun createNotificationChannelGroup(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -62,4 +62,7 @@ object BundleKeys {
|
|||||||
val KEY_FILE_ID = "KEY_FILE_ID"
|
val KEY_FILE_ID = "KEY_FILE_ID"
|
||||||
val KEY_NOTIFICATION_ID = "KEY_NOTIFICATION_ID"
|
val KEY_NOTIFICATION_ID = "KEY_NOTIFICATION_ID"
|
||||||
val KEY_CONVERSATION_ID = "KEY_CONVERSATION_ID"
|
val KEY_CONVERSATION_ID = "KEY_CONVERSATION_ID"
|
||||||
|
|
||||||
|
val KEY_DECRYPTED_PUSH_MESSAGE = "KEY_DECRYPTED_PUSH_MESSAGE"
|
||||||
|
val KEY_SIGNATURE_VERIFICATION = "KEY_SIGNATURE_VERIFICATION"
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user