diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dc3cbc5e8..279b00951 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -198,6 +198,8 @@ + + getNotification(@Header("Authorization") String authorization, - @Url String url); + Observable getNcNotification(@Header("Authorization") String authorization, + @Url String url); @FormUrlEncoded @POST @@ -595,4 +595,12 @@ public interface NcApi { @DELETE Observable withdrawRequestAssistance(@Header("Authorization") String authorization, @Url String url); + + @POST + Observable sendCommonPostRequest(@Header("Authorization") String authorization, + @Url String url); + + @DELETE + Observable sendCommonDeleteRequest(@Header("Authorization") String authorization, + @Url String url); } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt index 35b2d86f4..ff9b18b10 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/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 + * Copyright (C) 2022-2023 Marcel Hibbe * Copyright (C) 2022 Andy Scherzinger * Copyright (C) 2017-2018 Mario Danic * @@ -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 + if (ncNotification != null) { + enrichPushMessageByNcNotificationData(ncNotification) + // val newIntent = enrichIntentByNcNotificationData(intent, ncNotification) + showNotification(intent, ncNotification) } - - 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 - } - } - 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 diff --git a/app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt new file mode 100644 index 000000000..8edea6242 --- /dev/null +++ b/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 + * + * 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 . + */ + +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 { + 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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt new file mode 100644 index 000000000..1abe36622 --- /dev/null +++ b/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 + * + * 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 . + */ + +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(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 { + 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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index 6c2dd557b..e649b1a3b 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/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; } diff --git a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt index c2745cd51..a5682d959 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt +++ b/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) } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/VibrationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/VibrationUtils.kt index 6753c46e7..4f2c2254c 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/VibrationUtils.kt +++ b/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 diff --git a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt index e89647e3c..efd7ee0df 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt +++ b/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" }