Pārlūkot izejas kodu

Merge pull request #2795 from nextcloud/feature/2722/addRecordingFileNotification

Feature/2722/add recording file notification
Marcel Hibbe 2 gadi atpakaļ
vecāks
revīzija
8ae9985009

+ 2 - 0
app/src/main/AndroidManifest.xml

@@ -198,6 +198,8 @@
 
         <receiver android:name=".receivers.DirectReplyReceiver" />
         <receiver android:name=".receivers.MarkAsReadReceiver" />
+        <receiver android:name=".receivers.DismissRecordingAvailableReceiver" />
+        <receiver android:name=".receivers.ShareRecordingToChatReceiver" />
 
         <service
             android:name=".utils.SyncService"

+ 10 - 2
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -382,8 +382,8 @@ public interface NcApi {
                                                                @Url String url);
 
     @GET
-    Observable<NotificationOverall> getNotification(@Header("Authorization") String authorization,
-                                                    @Url String url);
+    Observable<NotificationOverall> getNcNotification(@Header("Authorization") String authorization,
+                                                      @Url String url);
 
     @FormUrlEncoded
     @POST
@@ -595,4 +595,12 @@ public interface NcApi {
     @DELETE
     Observable<GenericOverall> withdrawRequestAssistance(@Header("Authorization") String authorization,
                                              @Url String url);
+
+    @POST
+    Observable<GenericOverall> sendCommonPostRequest(@Header("Authorization") String authorization,
+                                                 @Url String url);
+
+    @DELETE
+    Observable<GenericOverall> sendCommonDeleteRequest(@Header("Authorization") String authorization,
+                                                     @Url String url);
 }

+ 267 - 134
app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt

@@ -4,7 +4,7 @@
  * @author Andy Scherzinger
  * @author Mario Danic
  * @author Marcel Hibbe
- * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ * Copyright (C) 2022-2023 Marcel Hibbe <dev@mhibbe.de>
  * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
  * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
  *
@@ -67,7 +67,9 @@ import com.nextcloud.talk.models.json.participants.ParticipantsOverall
 import com.nextcloud.talk.models.json.push.DecryptedPushMessage
 import com.nextcloud.talk.models.json.push.NotificationUser
 import com.nextcloud.talk.receivers.DirectReplyReceiver
+import com.nextcloud.talk.receivers.DismissRecordingAvailableReceiver
 import com.nextcloud.talk.receivers.MarkAsReadReceiver
+import com.nextcloud.talk.receivers.ShareRecordingToChatReceiver
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DoNotDisturbUtils.shouldPlaySound
 import com.nextcloud.talk.utils.NotificationUtils
@@ -79,12 +81,15 @@ import com.nextcloud.talk.utils.NotificationUtils.getMessageRingtoneUri
 import com.nextcloud.talk.utils.NotificationUtils.loadAvatarSync
 import com.nextcloud.talk.utils.PushUtils
 import com.nextcloud.talk.utils.bundle.BundleKeys
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_DISMISS_RECORDING_URL
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MESSAGE_ID
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_ID
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_RESTRICT_DELETION
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_TIMESTAMP
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SHARE_RECORDING_TO_CHAT_URL
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
 import com.nextcloud.talk.utils.preferences.AppPreferences
@@ -164,9 +169,8 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
         } else if (isSpreedNotification()) {
             Log.d(TAG, "pushMessage.type: " + pushMessage.type)
             when (pushMessage.type) {
-                "chat" -> handleChatNotification()
-                "room" -> handleRoomNotification()
-                "call" -> handleCallNotification()
+                TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING -> handleNonCallPushMessage()
+                TYPE_CALL -> handleCallPushMessage()
                 else -> Log.e(TAG, "unknown pushMessage.type")
             }
         } else {
@@ -176,38 +180,16 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
         return Result.success()
     }
 
-    private fun handleChatNotification() {
-        val chatIntent = Intent(context, MainActivity::class.java)
-        chatIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
-        val chatBundle = Bundle()
-        chatBundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
-        chatBundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user)
-        chatBundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false)
-        chatIntent.putExtras(chatBundle)
+    private fun handleNonCallPushMessage() {
+        val mainActivityIntent = createMainActivityIntent()
         if (pushMessage.notificationId != Long.MIN_VALUE) {
-            showNotificationWithObjectData(chatIntent)
+            getNcDataAndShowNotification(mainActivityIntent)
         } else {
-            showNotification(chatIntent)
+            showNotification(mainActivityIntent, null)
         }
     }
 
-    /**
-     * handle messages with type 'room', e.g. "xxx invited you to a group conversation"
-     */
-    private fun handleRoomNotification() {
-        val intent = Intent(context, MainActivity::class.java)
-        intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
-        val bundle = Bundle()
-        bundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
-        bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user)
-        bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false)
-        intent.putExtras(bundle)
-        if (bundle.containsKey(KEY_ROOM_TOKEN)) {
-            showNotificationWithObjectData(intent)
-        }
-    }
-
-    private fun handleCallNotification() {
+    private fun handleCallPushMessage() {
         val fullScreenIntent = Intent(context, CallNotificationActivity::class.java)
         val bundle = Bundle()
         bundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
@@ -313,13 +295,13 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
 
     private fun isSpreedNotification() = SPREED_APP == pushMessage.app
 
-    private fun showNotificationWithObjectData(intent: Intent) {
+    private fun getNcDataAndShowNotification(intent: Intent) {
         val user = signatureVerification.user
 
         // see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md
-        ncApi.getNotification(
+        ncApi.getNcNotification(
             credentials,
-            ApiUtils.getUrlForNotificationWithId(
+            ApiUtils.getUrlForNcNotificationWithId(
                 user!!.baseUrl,
                 (pushMessage.notificationId!!).toString()
             )
@@ -331,58 +313,15 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
 
                 override fun onNext(notificationOverall: NotificationOverall) {
                     val ncNotification = notificationOverall.ocs!!.notification
-
-                    if (ncNotification!!.messageRichParameters != null &&
-                        ncNotification.messageRichParameters!!.size > 0
-                    ) {
-                        pushMessage.text = getParsedMessage(
-                            ncNotification.messageRich,
-                            ncNotification.messageRichParameters
-                        )
-                    } else {
-                        pushMessage.text = ncNotification.message
-                    }
-
-                    val subjectRichParameters = ncNotification.subjectRichParameters
-
-                    pushMessage.timestamp = ncNotification.datetime!!.millis
-
-                    if (subjectRichParameters != null && subjectRichParameters.size > 0) {
-                        val callHashMap = subjectRichParameters["call"]
-                        val userHashMap = subjectRichParameters["user"]
-                        val guestHashMap = subjectRichParameters["guest"]
-                        if (callHashMap != null && callHashMap.size > 0 && callHashMap.containsKey("name")) {
-                            if (subjectRichParameters.containsKey("reaction")) {
-                                pushMessage.subject = ""
-                                pushMessage.text = ncNotification.subject
-                            } else if (ncNotification.objectType == "chat") {
-                                pushMessage.subject = callHashMap["name"]!!
-                            } else {
-                                pushMessage.subject = ncNotification.subject!!
-                            }
-                            if (callHashMap.containsKey("call-type")) {
-                                conversationType = callHashMap["call-type"]
-                            }
-                        }
-                        val notificationUser = NotificationUser()
-                        if (userHashMap != null && userHashMap.isNotEmpty()) {
-                            notificationUser.id = userHashMap["id"]
-                            notificationUser.type = userHashMap["type"]
-                            notificationUser.name = userHashMap["name"]
-                            pushMessage.notificationUser = notificationUser
-                        } else if (guestHashMap != null && guestHashMap.isNotEmpty()) {
-                            notificationUser.id = guestHashMap["id"]
-                            notificationUser.type = guestHashMap["type"]
-                            notificationUser.name = guestHashMap["name"]
-                            pushMessage.notificationUser = notificationUser
-                        }
+                    if (ncNotification != null) {
+                        enrichPushMessageByNcNotificationData(ncNotification)
+                        // val newIntent = enrichIntentByNcNotificationData(intent, ncNotification)
+                        showNotification(intent, ncNotification)
                     }
-                    pushMessage.objectId = ncNotification.objectId
-                    showNotification(intent)
                 }
 
                 override fun onError(e: Throwable) {
-                    // unused atm
+                    Log.e(TAG, "Failed to get notification", e)
                 }
 
                 override fun onComplete() {
@@ -391,30 +330,81 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
             })
     }
 
-    @Suppress("MagicNumber")
-    private fun showNotification(intent: Intent) {
-        val largeIcon: Bitmap
-        val priority = NotificationCompat.PRIORITY_HIGH
-        val smallIcon: Int = R.drawable.ic_logo
-        val category: String = if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
-            Notification.CATEGORY_MESSAGE
+    private fun enrichIntentByNcNotificationData(
+        intent: Intent,
+        ncNotification: com.nextcloud.talk.models.json.notifications.Notification
+    ): Intent {
+        val newIntent = Intent(intent)
+
+        return newIntent
+    }
+
+    private fun enrichPushMessageByNcNotificationData(
+        ncNotification: com.nextcloud.talk.models.json.notifications.Notification
+    ) {
+        pushMessage.objectId = ncNotification.objectId
+        pushMessage.timestamp = ncNotification.datetime!!.millis
+
+        if (ncNotification.messageRichParameters != null &&
+            ncNotification.messageRichParameters!!.size > 0
+        ) {
+            pushMessage.text = getParsedMessage(
+                ncNotification.messageRich,
+                ncNotification.messageRichParameters
+            )
         } else {
-            Notification.CATEGORY_CALL
+            pushMessage.text = ncNotification.message
         }
-        when (conversationType) {
-            "one2one" -> {
-                pushMessage.subject = ""
-                largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_people_group_black_24px)?.toBitmap()!!
-            }
-            "group" ->
-                largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_people_group_black_24px)?.toBitmap()!!
-            "public" -> largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_link_black_24px)?.toBitmap()!!
-            else -> // assuming one2one
-                largeIcon = if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
-                    ContextCompat.getDrawable(context!!, R.drawable.ic_comment)?.toBitmap()!!
+
+        val subjectRichParameters = ncNotification.subjectRichParameters
+        if (subjectRichParameters != null && subjectRichParameters.size > 0) {
+            val callHashMap = subjectRichParameters["call"]
+            val userHashMap = subjectRichParameters["user"]
+            val guestHashMap = subjectRichParameters["guest"]
+            if (callHashMap != null && callHashMap.size > 0 && callHashMap.containsKey("name")) {
+                if (subjectRichParameters.containsKey("reaction")) {
+                    pushMessage.subject = ""
+                } else if (ncNotification.objectType == "chat") {
+                    pushMessage.subject = callHashMap["name"]!!
                 } else {
-                    ContextCompat.getDrawable(context!!, R.drawable.ic_call_black_24dp)?.toBitmap()!!
+                    pushMessage.subject = ncNotification.subject!!
+                }
+
+                if (subjectRichParameters.containsKey("reaction")) {
+                    pushMessage.text = ncNotification.subject
                 }
+
+                if (callHashMap.containsKey("call-type")) {
+                    conversationType = callHashMap["call-type"]
+                }
+            }
+            val notificationUser = NotificationUser()
+            if (userHashMap != null && userHashMap.isNotEmpty()) {
+                notificationUser.id = userHashMap["id"]
+                notificationUser.type = userHashMap["type"]
+                notificationUser.name = userHashMap["name"]
+                pushMessage.notificationUser = notificationUser
+            } else if (guestHashMap != null && guestHashMap.isNotEmpty()) {
+                notificationUser.id = guestHashMap["id"]
+                notificationUser.type = guestHashMap["type"]
+                notificationUser.name = guestHashMap["name"]
+                pushMessage.notificationUser = notificationUser
+            }
+        } else {
+            pushMessage.subject = ncNotification.subject.orEmpty()
+        }
+    }
+
+    @Suppress("MagicNumber")
+    private fun showNotification(
+        intent: Intent,
+        ncNotification: com.nextcloud.talk.models.json.notifications.Notification?
+    ) {
+        var category = ""
+        when (pushMessage.type) {
+            TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING -> category = Notification.CATEGORY_MESSAGE
+            TYPE_CALL -> category = Notification.CATEGORY_CALL
+            else -> Log.e(TAG, "unknown pushMessage.type")
         }
 
         // Use unique request code to make sure that a new PendingIntent gets created for each notification
@@ -428,41 +418,52 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
         val pendingIntent = PendingIntent.getActivity(context, requestCode, intent, intentFlag)
         val uri = Uri.parse(signatureVerification.user!!.baseUrl)
         val baseUrl = uri.host
+
+        var contentTitle: CharSequence? = ""
+        if (!TextUtils.isEmpty(pushMessage.subject)) {
+            contentTitle = EmojiCompat.get().process(pushMessage.subject)
+        }
+
+        var contentText: CharSequence? = ""
+        if (!TextUtils.isEmpty(pushMessage.text)) {
+            contentText = EmojiCompat.get().process(pushMessage.text!!)
+        }
+
+        val autoCancelOnClick = TYPE_RECORDING != pushMessage.type
+
         val notificationBuilder = NotificationCompat.Builder(context!!, "1")
-            .setLargeIcon(largeIcon)
-            .setSmallIcon(smallIcon)
+            .setPriority(NotificationCompat.PRIORITY_HIGH)
             .setCategory(category)
-            .setPriority(priority)
+            .setLargeIcon(getLargeIcon())
+            .setSmallIcon(R.drawable.ic_logo)
+            .setContentTitle(contentTitle)
+            .setContentText(contentText)
             .setSubText(baseUrl)
             .setWhen(pushMessage.timestamp)
             .setShowWhen(true)
             .setContentIntent(pendingIntent)
-            .setAutoCancel(true)
-        if (!TextUtils.isEmpty(pushMessage.subject)) {
-            notificationBuilder.setContentTitle(
-                EmojiCompat.get().process(pushMessage.subject)
-            )
-        }
-        if (!TextUtils.isEmpty(pushMessage.text)) {
-            notificationBuilder.setContentText(
-                EmojiCompat.get().process(pushMessage.text!!)
-            )
-        }
-
-        notificationBuilder.color = context!!.resources.getColor(R.color.colorPrimary)
+            .setAutoCancel(autoCancelOnClick)
+            .setColor(context!!.resources.getColor(R.color.colorPrimary))
 
         val notificationInfoBundle = Bundle()
         notificationInfoBundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!)
         // could be an ID or a TOKEN
         notificationInfoBundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
         notificationInfoBundle.putLong(KEY_NOTIFICATION_ID, pushMessage.notificationId!!)
+
+        if (pushMessage.type == TYPE_RECORDING) {
+            notificationInfoBundle.putBoolean(KEY_NOTIFICATION_RESTRICT_DELETION, true)
+        }
+
         notificationBuilder.setExtras(notificationInfoBundle)
 
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
-                notificationBuilder.setChannelId(
-                    NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name
-                )
+            when (pushMessage.type) {
+                TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING -> {
+                    notificationBuilder.setChannelId(
+                        NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name
+                    )
+                }
             }
         } else {
             // red color for the lights
@@ -482,14 +483,54 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
         // It is NOT the same as the notification ID used in communication with the server.
         val systemNotificationId: Int =
             activeStatusBarNotification?.id ?: calculateCRC32(System.currentTimeMillis().toString()).toInt()
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && CHAT == pushMessage.type &&
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
+            TYPE_CHAT == pushMessage.type &&
             pushMessage.notificationUser != null
         ) {
             prepareChatNotification(notificationBuilder, activeStatusBarNotification, systemNotificationId)
+            addReplyAction(notificationBuilder, systemNotificationId)
+            addMarkAsReadAction(notificationBuilder, systemNotificationId)
+        }
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
+            TYPE_RECORDING == pushMessage.type &&
+            ncNotification != null
+        ) {
+            addDismissRecordingAvailableAction(notificationBuilder, systemNotificationId, ncNotification)
+            addShareRecordingToChatAction(notificationBuilder, systemNotificationId, ncNotification)
         }
         sendNotification(systemNotificationId, notificationBuilder.build())
     }
 
+    private fun getLargeIcon(): Bitmap {
+        val largeIcon: Bitmap
+        if (pushMessage.type == TYPE_RECORDING) {
+            largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_baseline_videocam_24)?.toBitmap()!!
+        } else {
+            when (conversationType) {
+                "one2one" -> {
+                    pushMessage.subject = ""
+                    largeIcon =
+                        ContextCompat.getDrawable(context!!, R.drawable.ic_people_group_black_24px)?.toBitmap()!!
+                }
+                "group" ->
+                    largeIcon =
+                        ContextCompat.getDrawable(context!!, R.drawable.ic_people_group_black_24px)?.toBitmap()!!
+                "public" ->
+                    largeIcon =
+                        ContextCompat.getDrawable(context!!, R.drawable.ic_link_black_24px)?.toBitmap()!!
+                else -> // assuming one2one
+                    largeIcon = if (TYPE_CHAT == pushMessage.type || TYPE_ROOM == pushMessage.type) {
+                        ContextCompat.getDrawable(context!!, R.drawable.ic_comment)?.toBitmap()!!
+                    } else {
+                        ContextCompat.getDrawable(context!!, R.drawable.ic_call_black_24dp)?.toBitmap()!!
+                    }
+            }
+        }
+        return largeIcon
+    }
+
     private fun calculateCRC32(s: String): Long {
         val crc32 = CRC32()
         crc32.update(s.toByteArray())
@@ -515,8 +556,6 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
             .setName(EmojiCompat.get().process(notificationUser.name!!))
             .setBot("bot" == userType)
         notificationBuilder.setOnlyAlertOnce(true)
-        addReplyAction(notificationBuilder, systemNotificationId)
-        addMarkAsReadAction(notificationBuilder, systemNotificationId)
 
         if ("user" == userType || "guest" == userType) {
             val baseUrl = signatureVerification.user!!.baseUrl
@@ -566,7 +605,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
                 systemNotificationId,
                 messageId
             )
-            val action = NotificationCompat.Action.Builder(
+            val markAsReadAction = NotificationCompat.Action.Builder(
                 R.drawable.ic_eye,
                 context!!.resources.getString(R.string.nc_mark_as_read),
                 pendingIntent
@@ -574,7 +613,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
                 .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
                 .setShowsUserInterface(false)
                 .build()
-            notificationBuilder.addAction(action)
+            notificationBuilder.addAction(markAsReadAction)
         }
     }
 
@@ -585,7 +624,11 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
             .setLabel(replyLabel)
             .build()
 
-        val replyPendingIntent = buildIntentForAction(DirectReplyReceiver::class.java, systemNotificationId, 0)
+        val replyPendingIntent = buildIntentForAction(
+            DirectReplyReceiver::class.java,
+            systemNotificationId,
+            0
+        )
         val replyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply, replyLabel, replyPendingIntent)
             .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
             .setShowsUserInterface(false)
@@ -595,6 +638,85 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
         notificationBuilder.addAction(replyAction)
     }
 
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    private fun addDismissRecordingAvailableAction(
+        notificationBuilder: NotificationCompat.Builder,
+        systemNotificationId: Int,
+        ncNotification: com.nextcloud.talk.models.json.notifications.Notification
+    ) {
+        var dismissLabel = ""
+        var dismissRecordingUrl = ""
+
+        for (action in ncNotification.actions!!) {
+            if (!action.primary) {
+                dismissLabel = action.label.orEmpty()
+                dismissRecordingUrl = action.link.orEmpty()
+            }
+        }
+
+        val dismissIntent = Intent(context, DismissRecordingAvailableReceiver::class.java)
+        dismissIntent.putExtra(KEY_SYSTEM_NOTIFICATION_ID, systemNotificationId)
+        dismissIntent.putExtra(KEY_DISMISS_RECORDING_URL, dismissRecordingUrl)
+
+        val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+        } else {
+            PendingIntent.FLAG_UPDATE_CURRENT
+        }
+        val dismissPendingIntent = PendingIntent.getBroadcast(context, systemNotificationId, dismissIntent, intentFlag)
+
+        val dismissAction = NotificationCompat.Action.Builder(R.drawable.ic_delete, dismissLabel, dismissPendingIntent)
+            .setShowsUserInterface(false)
+            .setAllowGeneratedReplies(true)
+            .build()
+        notificationBuilder.addAction(dismissAction)
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    private fun addShareRecordingToChatAction(
+        notificationBuilder: NotificationCompat.Builder,
+        systemNotificationId: Int,
+        ncNotification: com.nextcloud.talk.models.json.notifications.Notification
+    ) {
+        var shareToChatLabel = ""
+        var shareToChatUrl = ""
+
+        for (action in ncNotification.actions!!) {
+            if (action.primary) {
+                shareToChatLabel = action.label.orEmpty()
+                shareToChatUrl = action.link.orEmpty()
+            }
+        }
+
+        val shareRecordingIntent = Intent(context, ShareRecordingToChatReceiver::class.java)
+        shareRecordingIntent.putExtra(KEY_SYSTEM_NOTIFICATION_ID, systemNotificationId)
+        shareRecordingIntent.putExtra(KEY_SHARE_RECORDING_TO_CHAT_URL, shareToChatUrl)
+        shareRecordingIntent.putExtra(KEY_ROOM_TOKEN, pushMessage.id)
+        shareRecordingIntent.putExtra(KEY_USER_ENTITY, signatureVerification.user)
+
+        val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+        } else {
+            PendingIntent.FLAG_UPDATE_CURRENT
+        }
+        val shareRecordingPendingIntent = PendingIntent.getBroadcast(
+            context,
+            systemNotificationId,
+            shareRecordingIntent,
+            intentFlag
+        )
+
+        val shareRecordingAction = NotificationCompat.Action.Builder(
+            R.drawable.ic_delete,
+            shareToChatLabel,
+            shareRecordingPendingIntent
+        )
+            .setShowsUserInterface(false)
+            .setAllowGeneratedReplies(true)
+            .build()
+        notificationBuilder.addAction(shareRecordingAction)
+    }
+
     @RequiresApi(api = Build.VERSION_CODES.N)
     private fun getStyle(person: Person, style: NotificationCompat.MessagingStyle?): NotificationCompat.MessagingStyle {
         val newStyle = NotificationCompat.MessagingStyle(person)
@@ -641,7 +763,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
             ) {
                 val audioAttributesBuilder =
                     AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
-                if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
+                if (TYPE_CHAT == pushMessage.type || TYPE_ROOM == pushMessage.type) {
                     audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
                 } else {
                     audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST)
@@ -810,15 +932,24 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
         }
     }
 
-    private fun getIntentToOpenConversation(): PendingIntent? {
-        val bundle = Bundle()
+    private fun createMainActivityIntent(): Intent {
         val intent = Intent(context, MainActivity::class.java)
         intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
-
+        val bundle = Bundle()
         bundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
         bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user)
         bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false)
+        intent.putExtras(bundle)
+        return intent
+    }
 
+    private fun getIntentToOpenConversation(): PendingIntent? {
+        val intent = Intent(context, MainActivity::class.java)
+        intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
+        val bundle = Bundle()
+        bundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
+        bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user)
+        bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false)
         intent.putExtras(bundle)
 
         val requestCode = System.currentTimeMillis().toInt()
@@ -832,8 +963,10 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
 
     companion object {
         val TAG = NotificationWorker::class.simpleName
-        private const val CHAT = "chat"
-        private const val ROOM = "room"
+        private const val TYPE_CHAT = "chat"
+        private const val TYPE_ROOM = "room"
+        private const val TYPE_CALL = "call"
+        private const val TYPE_RECORDING = "recording"
         private const val SPREED_APP = "spreed"
         private const val TIMER_START = 1
         private const val TIMER_COUNT = 12

+ 109 - 0
app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt

@@ -0,0 +1,109 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022-2023 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * 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.receivers
+
+import android.app.NotificationManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import autodagger.AutoInjector
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.models.json.generic.GenericOverall
+import com.nextcloud.talk.users.UserManager
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.bundle.BundleKeys
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class DismissRecordingAvailableReceiver : BroadcastReceiver() {
+
+    @Inject
+    lateinit var userManager: UserManager
+
+    @Inject
+    lateinit var ncApi: NcApi
+
+    lateinit var context: Context
+    lateinit var currentUser: User
+    private var systemNotificationId: Int? = null
+    private var link: String? = null
+
+    init {
+        NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+    }
+
+    override fun onReceive(receiveContext: Context, intent: Intent?) {
+        context = receiveContext
+
+        // NOTE - systemNotificationId is an internal ID used on the device only.
+        // It is NOT the same as the notification ID used in communication with the server.
+        systemNotificationId = intent!!.getIntExtra(KEY_SYSTEM_NOTIFICATION_ID, 0)
+        link = intent.getStringExtra(BundleKeys.KEY_DISMISS_RECORDING_URL)
+
+        val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, userManager.currentUser.blockingGet().id!!)
+        currentUser = userManager.getUserWithId(id).blockingGet()
+
+        dismissNcRecordingAvailableNotification()
+    }
+
+    private fun dismissNcRecordingAvailableNotification() {
+        val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)
+
+        ncApi.sendCommonDeleteRequest(credentials, link)
+            ?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<GenericOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(genericOverall: GenericOverall) {
+                    cancelNotification(systemNotificationId!!)
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, "Failed to send dismiss for recording available", e)
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    private fun cancelNotification(notificationId: Int) {
+        val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+        notificationManager.cancel(notificationId)
+    }
+
+    companion object {
+        private val TAG = DismissRecordingAvailableReceiver::class.java.simpleName
+    }
+}

+ 126 - 0
app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt

@@ -0,0 +1,126 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022-2023 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * 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.receivers
+
+import android.app.NotificationManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import android.widget.Toast
+import autodagger.AutoInjector
+import com.nextcloud.talk.R
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.models.json.generic.GenericOverall
+import com.nextcloud.talk.users.UserManager
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.bundle.BundleKeys
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class ShareRecordingToChatReceiver : BroadcastReceiver() {
+
+    @Inject
+    lateinit var userManager: UserManager
+
+    @Inject
+    lateinit var ncApi: NcApi
+
+    lateinit var context: Context
+    lateinit var currentUser: User
+    private var systemNotificationId: Int? = null
+    private var link: String? = null
+    var roomToken: String? = null
+    var conversationOfShareTarget: User? = null
+
+    init {
+        NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+    }
+
+    override fun onReceive(receiveContext: Context, intent: Intent?) {
+        context = receiveContext
+        systemNotificationId = intent!!.getIntExtra(KEY_SYSTEM_NOTIFICATION_ID, 0)
+        link = intent.getStringExtra(BundleKeys.KEY_SHARE_RECORDING_TO_CHAT_URL)
+
+        roomToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)
+        conversationOfShareTarget = intent.getParcelableExtra<User>(BundleKeys.KEY_USER_ENTITY)
+
+        val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, userManager.currentUser.blockingGet().id!!)
+        currentUser = userManager.getUserWithId(id).blockingGet()
+
+        shareRecordingToChat()
+    }
+
+    private fun shareRecordingToChat() {
+        val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)
+
+        ncApi.sendCommonPostRequest(credentials, link)
+            ?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<GenericOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(genericOverall: GenericOverall) {
+                    cancelNotification(systemNotificationId!!)
+
+                    // Here it would make sense to open the chat where the recording was shared to (startActivity...).
+                    // However, as we are in a broadcast receiver, this needs a TaskStackBuilder
+                    // combined with addNextIntentWithParentStack. For further reading, see
+                    // https://developer.android.com/develop/ui/views/notifications/navigation#DirectEntry
+                    // As we are using the conductor framework it might be hard the combine this or to keep an overview.
+                    // For this reason there is only a toast for now until we got rid of conductor.
+
+                    Toast.makeText(
+                        context,
+                        context.resources.getString(R.string.nc_all_ok_operation),
+                        Toast.LENGTH_LONG
+                    ).show()
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, "Failed to share recording to chat request", e)
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    private fun cancelNotification(notificationId: Int) {
+        val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+        notificationManager.cancel(notificationId)
+    }
+
+    companion object {
+        private val TAG = ShareRecordingToChatReceiver::class.java.simpleName
+    }
+}

+ 1 - 1
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java

@@ -392,7 +392,7 @@ public class ApiUtils {
     }
 
     // see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md
-    public static String getUrlForNotificationWithId(String baseUrl, String notificationId) {
+    public static String getUrlForNcNotificationWithId(String baseUrl, String notificationId) {
         return baseUrl + ocsApiVersion + "/apps/notifications/api/v2/notifications/" + notificationId;
     }
 

+ 3 - 1
app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt

@@ -267,7 +267,9 @@ object NotificationUtils {
 
     fun cancelExistingNotificationsForRoom(context: Context?, conversationUser: User, roomTokenOrId: String) {
         scanNotifications(context, conversationUser) { notificationManager, statusBarNotification, notification ->
-            if (roomTokenOrId == notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN)) {
+            if (roomTokenOrId == notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN) &&
+                !notification.extras.getBoolean(BundleKeys.KEY_NOTIFICATION_RESTRICT_DELETION)
+            ) {
                 notificationManager.cancel(statusBarNotification.id)
             }
         }

+ 1 - 1
app/src/main/java/com/nextcloud/talk/utils/VibrationUtils.kt

@@ -25,7 +25,7 @@ import android.os.VibrationEffect
 import android.os.Vibrator
 
 object VibrationUtils {
-    private const val SHORT_VIBRATE: Long = 20
+    private const val SHORT_VIBRATE: Long = 100
 
     fun vibrateShort(context: Context) {
         val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator

+ 3 - 0
app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt

@@ -82,4 +82,7 @@ object BundleKeys {
     const val KEY_IS_MODERATOR = "KEY_IS_MODERATOR"
     const val KEY_SWITCH_TO_ROOM_AND_START_CALL = "KEY_SWITCH_TO_ROOM_AND_START_CALL"
     const val KEY_IS_BREAKOUT_ROOM = "KEY_IS_BREAKOUT_ROOM"
+    const val KEY_NOTIFICATION_RESTRICT_DELETION = "KEY_NOTIFICATION_RESTRICT_DELETION"
+    const val KEY_DISMISS_RECORDING_URL = "KEY_DISMISS_RECORDING_URL"
+    const val KEY_SHARE_RECORDING_TO_CHAT_URL = "KEY_SHARE_RECORDING_TO_CHAT_URL"
 }