Lots of progress on new notifications system

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2020-02-23 14:11:39 +01:00
parent 11f013a762
commit 8252240b3b
No known key found for this signature in database
GPG Key ID: CDE0BBD2738C4CC0
7 changed files with 225 additions and 132 deletions

View File

@ -44,12 +44,14 @@ import com.nextcloud.talk.jobs.MessageNotificationWorker
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.newarch.services.CallService
import com.nextcloud.talk.newarch.utils.MagicJson
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_INCOMING_PUSH_MESSSAGE
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_OPEN_INCOMING_CALL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
import com.nextcloud.talk.utils.preferences.AppPreferences
@ -68,23 +70,6 @@ import javax.crypto.NoSuchPaddingException
class MagicFirebaseMessagingService : FirebaseMessagingService(), KoinComponent {
val tag: String = "MagicFirebaseMessagingService"
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) {
super.onNewToken(token)
@ -93,110 +78,14 @@ class MagicFirebaseMessagingService : FirebaseMessagingService(), KoinComponent
@SuppressLint("LongLogTag")
override fun onMessageReceived(remoteMessage: RemoteMessage) {
remoteMessage.data.let {
decryptMessage(it["subject"]!!, it["signature"]!!)
}
val incomingCallIntent = Intent(applicationContext, CallService::class.java)
incomingCallIntent.action = KEY_INCOMING_PUSH_MESSSAGE
incomingCallIntent.putExtra(BundleKeys.KEY_ENCRYPTED_SUBJECT, remoteMessage.data["subject"])
incomingCallIntent.putExtra(BundleKeys.KEY_ENCRYPTED_SIGNATURE, remoteMessage.data["signature"])
applicationContext.startService(incomingCallIntent)
}
@SuppressLint("LongLogTag")
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 {
when {
delete -> {
cancelExistingNotificationWithId(applicationContext, signatureVerification.userEntity!!, notificationId!!)
}
deleteAll -> {
cancelAllNotificationsForAccount(applicationContext, signatureVerification.userEntity!!)
}
type == "call" -> {
val fullScreenIntent = Intent(applicationContext, MagicCallActivity::class.java)
fullScreenIntent.action = KEY_OPEN_INCOMING_CALL
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_ROOM_ID, decryptedPushMessage.id)
bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.userEntity)
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(System.currentTimeMillis())
.setContentTitle(EmojiCompat.get().process(decryptedPushMessage.subject.toString()))
.setAutoCancel(true)
.setOngoing(true)
//.setTimeoutAfter(45000L)
.setContentIntent(fullScreenPendingIntent)
.setFullScreenIntent(fullScreenPendingIntent, true)
.setSound(NotificationUtils.getCallSoundUri(applicationContext, appPreferences), AudioManager.STREAM_RING)
if (vibrationEffect != null) {
notificationBuilder.setVibrate(vibrationEffect)
}
val notification = notificationBuilder.build()
notification.flags = notification.flags or Notification.FLAG_INSISTENT
isServiceInForeground = true
checkIfCallIsActive(signatureVerification, decryptedPushMessage)
startForeground(timestamp.toInt(), notification)
}
else -> {
val json = Json(MagicJson.customJsonConfiguration)
val messageData = Data.Builder()
.putString(BundleKeys.KEY_DECRYPTED_PUSH_MESSAGE, LoganSquare.serialize(decryptedPushMessage))
.putString(BundleKeys.KEY_SIGNATURE_VERIFICATION, json.stringify(SignatureVerification.serializer(), signatureVerification))
.build()
val pushNotificationWork = OneTimeWorkRequest.Builder(MessageNotificationWorker::class.java).setInputData(messageData).build()
WorkManager.getInstance().enqueue(pushNotificationWork)
}
}
}
}
} catch (e1: NoSuchAlgorithmException) {
Log.d(tag, "No proper algorithm to decrypt the message " + e1.localizedMessage)
} catch (e1: NoSuchPaddingException) {
Log.d(tag, "No proper padding to decrypt the message " + e1.localizedMessage)
} catch (e1: InvalidKeyException) {
Log.d(tag, "Invalid private key " + e1.localizedMessage)
}
} catch (exception: Exception) {
Log.d(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)

View File

@ -125,6 +125,8 @@
<service
android:name="com.novoda.merlin.MerlinService"
android:exported="false" />
<service android:name=".newarch.services.CallService"
android:exported="false"/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}"

View File

@ -20,6 +20,7 @@
package com.nextcloud.talk.activities
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.view.View
@ -36,6 +37,7 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.controllers.CallController
import com.nextcloud.talk.controllers.CallNotificationController
import com.nextcloud.talk.events.ConfigurationChangeEvent
import com.nextcloud.talk.newarch.services.CallService
import com.nextcloud.talk.utils.bundle.BundleKeys
class MagicCallActivity : BaseActivity() {
@ -64,7 +66,13 @@ class MagicCallActivity : BaseActivity() {
router!!.setPopsLastView(false)
if (!router!!.hasRootController()) {
if (intent.getBooleanExtra(BundleKeys.KEY_OPEN_INCOMING_CALL, false)) {
if (intent.action == BundleKeys.KEY_OPEN_INCOMING_CALL) {
val hideIncomingCallNotificationIntent = Intent(applicationContext, CallService::class.java)
hideIncomingCallNotificationIntent.action = BundleKeys.KEY_SHOW_INCOMING_CALL
hideIncomingCallNotificationIntent.putExtra(BundleKeys.KEY_NOTIFICATION_ID, intent.getLongExtra(BundleKeys.KEY_NOTIFICATION_ID, -1))
applicationContext?.startService(hideIncomingCallNotificationIntent)
router!!.setRoot(
RouterTransaction.with(CallNotificationController(intent.extras!!))
.pushChangeHandler(HorizontalChangeHandler())

View File

@ -22,6 +22,7 @@ package com.nextcloud.talk.controllers
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.BitmapDrawable
@ -39,6 +40,7 @@ import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import butterknife.BindView
import butterknife.OnClick
import coil.api.load
@ -62,6 +64,7 @@ import com.nextcloud.talk.models.json.participants.Participant
import com.nextcloud.talk.models.json.participants.ParticipantsOverall
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.newarch.local.models.getCredentials
import com.nextcloud.talk.newarch.services.CallService
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DoNotDisturbUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
@ -80,7 +83,6 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.koin.android.ext.android.inject
import org.michaelevans.colorart.library.ColorArt
import org.parceler.Parcels
import java.io.IOException
class CallNotificationController(private val originalBundle: Bundle) : BaseController() {
@ -111,7 +113,7 @@ class CallNotificationController(private val originalBundle: Bundle) : BaseContr
@JvmField
@BindView(R.id.incomingTextRelativeLayout)
var incomingTextRelativeLayout: RelativeLayout? = null
private val roomId: String
private val conversationToken: String
private val userBeingCalled: UserNgEntity?
private val credentials: String?
private var currentConversation: Conversation? = null
@ -121,12 +123,12 @@ class CallNotificationController(private val originalBundle: Bundle) : BaseContr
private var handler: Handler? = null
init {
this.roomId = originalBundle.getString(BundleKeys.KEY_ROOM_ID, "")
this.currentConversation = Parcels.unwrap(originalBundle.getParcelable(BundleKeys.KEY_ROOM))
this.userBeingCalled = originalBundle.getParcelable(BundleKeys.KEY_USER_ENTITY)
credentials = ApiUtils.getCredentials(userBeingCalled!!.username, userBeingCalled.token)
this.conversationToken = originalBundle.getString(BundleKeys.KEY_CONVERSATION_TOKEN)!!
this.userBeingCalled = originalBundle.getParcelable(BundleKeys.KEY_USER_ENTITY)!!
credentials = userBeingCalled.getCredentials()
}
override fun inflateView(
inflater: LayoutInflater,
container: ViewGroup
@ -278,13 +280,9 @@ class CallNotificationController(private val originalBundle: Bundle) : BaseContr
handler = Handler()
}
if (currentConversation == null) {
handleFromNotification()
} else {
runAllThings()
}
runAllThings()
var importantConversation = false
/*var importantConversation = false
val arbitraryStorageEntity: ArbitraryStorageEntity? = arbitraryStorageUtils.getStorageSetting(
userBeingCalled!!.id!!,
"important_conversation",
@ -371,7 +369,7 @@ class CallNotificationController(private val originalBundle: Bundle) : BaseContr
vibrator!!.cancel()
}
}, 10000)
}
}*/
}
@Subscribe(threadMode = ThreadMode.MAIN)

View File

@ -0,0 +1,189 @@
package com.nextcloud.talk.newarch.services
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.media.AudioAttributes
import android.media.AudioManager
import android.net.Uri
import android.os.Bundle
import android.os.IBinder
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.OneTimeWorkRequest
import androidx.work.WorkManager
import com.bluelinelabs.logansquare.LoganSquare
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.MagicCallActivity
import com.nextcloud.talk.jobs.MessageNotificationWorker
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.newarch.utils.MagicJson
import com.nextcloud.talk.utils.NotificationUtils
import com.nextcloud.talk.utils.PushUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.preferences.AppPreferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import org.koin.core.KoinComponent
import org.koin.core.inject
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.security.PrivateKey
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
import kotlin.coroutines.CoroutineContext
class CallService : Service(), KoinComponent, CoroutineScope {
val tag: String = "CallService"
private var coroutineJob: Job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + coroutineJob
val appPreferences: AppPreferences by inject()
val usersRepository: UsersRepository by inject()
var currentlyActiveNotificationId = 0L
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.let {
if (intent.action == BundleKeys.KEY_INCOMING_PUSH_MESSSAGE) {
decryptMessage(intent.getStringExtra(BundleKeys.KEY_ENCRYPTED_SUBJECT), intent.getStringExtra(BundleKeys.KEY_ENCRYPTED_SIGNATURE))
} else if (intent.action == BundleKeys.KEY_REJECT_INCOMING_CALL || intent.action == BundleKeys.KEY_SHOW_INCOMING_CALL) {
if (intent.getLongExtra(BundleKeys.KEY_NOTIFICATION_ID, -1L) == currentlyActiveNotificationId) {
stopForeground(true)
} else {
// do nothing? :D
}
} else {
}
}
return START_NOT_STICKY
}
private fun decryptMessage(subject: String, signature: String) = coroutineContext.run {
launch {
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 {
when {
delete -> {
NotificationUtils.cancelExistingNotificationWithId(applicationContext, signatureVerification.userEntity!!, notificationId!!)
}
deleteAll -> {
NotificationUtils.cancelAllNotificationsForAccount(applicationContext, signatureVerification.userEntity!!)
}
type == "call" -> {
val timestamp = System.currentTimeMillis()
val fullScreenIntent = Intent(applicationContext, MagicCallActivity::class.java)
fullScreenIntent.action = BundleKeys.KEY_OPEN_INCOMING_CALL
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, decryptedPushMessage.id)
bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, signatureVerification.userEntity)
bundle.putLong(BundleKeys.KEY_NOTIFICATION_ID, timestamp)
fullScreenIntent.putExtras(bundle)
fullScreenIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
val fullScreenPendingIntent = PendingIntent.getActivity(this@CallService, 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 rejectCallIntent = Intent(this@CallService, CallService::class.java)
rejectCallIntent.action = BundleKeys.KEY_REJECT_INCOMING_CALL
rejectCallIntent.putExtra(BundleKeys.KEY_NOTIFICATION_ID, timestamp)
val rejectCallPendingIntent = PendingIntent.getService(this@CallService, 0, rejectCallIntent, 0)
val notificationBuilder = NotificationCompat.Builder(this@CallService, notificationChannelId)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setSmallIcon(R.drawable.ic_call_black_24dp)
.setSubText(userBaseUrl)
.setShowWhen(true)
.setWhen(timestamp)
.setContentTitle(EmojiCompat.get().process(decryptedPushMessage.subject.toString()))
.setAutoCancel(true)
.setOngoing(true)
.addAction(R.drawable.ic_call_end_white_24px, resources.getString(R.string.reject_call), rejectCallPendingIntent)
//.setTimeoutAfter(45000L)
.setFullScreenIntent(fullScreenPendingIntent, true)
.setSound(NotificationUtils.getCallSoundUri(applicationContext, appPreferences), AudioManager.STREAM_RING)
if (vibrationEffect != null) {
notificationBuilder.setVibrate(vibrationEffect)
}
val notification = notificationBuilder.build()
notification.flags = notification.flags or Notification.FLAG_INSISTENT
//checkIfCallIsActive(signatureVerification, decryptedPushMessage)
currentlyActiveNotificationId = timestamp
startForeground(timestamp.toInt(), notification)
}
else -> {
val json = Json(MagicJson.customJsonConfiguration)
val messageData = Data.Builder()
.putString(BundleKeys.KEY_DECRYPTED_PUSH_MESSAGE, LoganSquare.serialize(decryptedPushMessage))
.putString(BundleKeys.KEY_SIGNATURE_VERIFICATION, json.stringify(SignatureVerification.serializer(), signatureVerification))
.build()
val pushNotificationWork = OneTimeWorkRequest.Builder(MessageNotificationWorker::class.java).setInputData(messageData).build()
WorkManager.getInstance().enqueue(pushNotificationWork)
}
}
}
} else {
// do absolutely nothing
}
} catch (e1: NoSuchAlgorithmException) {
Log.d(tag, "No proper algorithm to decrypt the message " + e1.localizedMessage)
} catch (e1: NoSuchPaddingException) {
Log.d(tag, "No proper padding to decrypt the message " + e1.localizedMessage)
} catch (e1: InvalidKeyException) {
Log.d(tag, "Invalid private key " + e1.localizedMessage)
}
} catch (exception: Exception) {
Log.d(tag, "Something went very wrong " + exception.localizedMessage)
}
}
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
}

View File

@ -61,6 +61,12 @@ object BundleKeys {
val KEY_NOTIFICATION_ID = "KEY_NOTIFICATION_ID"
val KEY_CONVERSATION_ID = "KEY_CONVERSATION_ID"
val KEY_ENCRYPTED_SUBJECT = "KEY_ENCRYPTED_SUBJECT"
val KEY_ENCRYPTED_SIGNATURE = "KEY_ENCRYPTED_SIGNATURE"
val KEY_REJECT_INCOMING_CALL = "KEY_REJECT_INCOMING_CALL"
val KEY_SHOW_INCOMING_CALL = "KEY_SHOW_INCOMING_CALL"
val KEY_INCOMING_PUSH_MESSSAGE = "KEY_INCOMING_PUSH_MESSAGE"
val KEY_DECRYPTED_PUSH_MESSAGE = "KEY_DECRYPTED_PUSH_MESSAGE"
val KEY_SIGNATURE_VERIFICATION = "KEY_SIGNATURE_VERIFICATION"
}

View File

@ -344,4 +344,5 @@
<string name="nc_search_for_more">Search for more participants</string>
<string name="nc_new_group">New group</string>
<string name="nc_search_empty_contacts">Where did they all hide?</string>
<string name="reject_call">Reject </string>
</resources>