diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9e1b27e5c..be6102c29 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -199,6 +199,7 @@ </receiver> <receiver android:name=".receivers.DirectReplyReceiver" /> + <receiver android:name=".receivers.MarkAsReadReceiver" /> <service android:name=".utils.SyncService" diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java index 5588c0656..4491edd13 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java @@ -52,6 +52,7 @@ import com.nextcloud.talk.models.json.notifications.NotificationOverall; 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.MarkAsReadReceiver; import com.nextcloud.talk.utils.ApiUtils; import com.nextcloud.talk.utils.DoNotDisturbUtils; import com.nextcloud.talk.utils.NotificationUtils; @@ -133,8 +134,10 @@ public class NotificationWorker extends Worker { ArbitraryStorageEntity arbitraryStorageEntity; - if ((arbitraryStorageEntity = arbitraryStorageUtils.getStorageSetting(userEntity.getId(), - "important_conversation", intent.getExtras().getString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN()))) != null) { + if ((arbitraryStorageEntity = arbitraryStorageUtils.getStorageSetting( + userEntity.getId(), + "important_conversation", + intent.getExtras().getString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN()))) != null) { importantConversation = Boolean.parseBoolean(arbitraryStorageEntity.getValue()); } @@ -201,8 +204,9 @@ public class NotificationWorker extends Worker { if (notification.getMessageRichParameters() != null && notification.getMessageRichParameters().size() > 0) { - decryptedPushMessage.setText(ChatUtils.Companion.getParsedMessage(notification.getMessageRich(), - notification.getMessageRichParameters())); + decryptedPushMessage.setText(ChatUtils.Companion.getParsedMessage( + notification.getMessageRich(), + notification.getMessageRichParameters())); } else { decryptedPushMessage.setText(notification.getMessage()); } @@ -246,6 +250,8 @@ public class NotificationWorker extends Worker { } } + decryptedPushMessage.setObjectId(notification.getObjectId()); + showNotification(intent); } @@ -332,10 +338,13 @@ public class NotificationWorker extends Worker { } Bundle notificationInfo = new Bundle(); - notificationInfo.putLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), signatureVerification.getUserEntity().getId()); + notificationInfo.putLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), + signatureVerification.getUserEntity().getId()); // could be an ID or a TOKEN - notificationInfo.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), decryptedPushMessage.getId()); - notificationInfo.putLong(BundleKeys.INSTANCE.getKEY_NOTIFICATION_ID(), decryptedPushMessage.getNotificationId()); + notificationInfo.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), + decryptedPushMessage.getId()); + notificationInfo.putLong(BundleKeys.INSTANCE.getKEY_NOTIFICATION_ID(), + decryptedPushMessage.getNotificationId()); notificationBuilder.setExtras(notificationInfo); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -401,6 +410,7 @@ public class NotificationWorker extends Worker { notificationBuilder.setOnlyAlertOnce(true); addReplyAction(notificationBuilder, systemNotificationId); + addMarkAsReadAction(notificationBuilder, systemNotificationId); if ("user".equals(userType) || "guest".equals(userType)) { String baseUrl = signatureVerification.getUserEntity().getBaseUrl(); @@ -413,6 +423,54 @@ public class NotificationWorker extends Worker { notificationBuilder.setStyle(getStyle(person.build(), style)); } + private PendingIntent buildIntentForAction(Class<?> cls, int systemNotificationId, int messageId) { + Intent actualIntent = new Intent(context, cls); + + // 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. + actualIntent.putExtra(BundleKeys.INSTANCE.getKEY_SYSTEM_NOTIFICATION_ID(), systemNotificationId); + actualIntent.putExtra(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), + Objects.requireNonNull(signatureVerification.getUserEntity()).getId()); + actualIntent.putExtra(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), decryptedPushMessage.getId()); + actualIntent.putExtra(BundleKeys.KEY_MESSAGE_ID, messageId); + + int intentFlag; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + intentFlag = PendingIntent.FLAG_MUTABLE|PendingIntent.FLAG_UPDATE_CURRENT; + } else { + intentFlag = PendingIntent.FLAG_UPDATE_CURRENT; + } + + return PendingIntent.getBroadcast(context, systemNotificationId, actualIntent, intentFlag); + } + + private void addMarkAsReadAction(NotificationCompat.Builder notificationBuilder, int systemNotificationId) { + if (decryptedPushMessage.getObjectId() != null) { + int messageId = 0; + try { + messageId = parseMessageId(decryptedPushMessage.getObjectId()); + } catch (NumberFormatException nfe) { + Log.e(TAG, "Failed to parse messageId from objectId, skip adding mark-as-read action.", nfe); + return; + } + + // Build a PendingIntent for the mark as read action + PendingIntent pendingIntent = buildIntentForAction(MarkAsReadReceiver.class, + systemNotificationId, + messageId); + + NotificationCompat.Action action = + new NotificationCompat.Action.Builder(R.drawable.ic_eye, + context.getResources().getString(R.string.nc_mark_as_read), + pendingIntent) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) + .setShowsUserInterface(false) + .build(); + + notificationBuilder.addAction(action); + } + } + @RequiresApi(api = Build.VERSION_CODES.N) private void addReplyAction(NotificationCompat.Builder notificationBuilder, int systemNotificationId) { String replyLabel = context.getResources().getString(R.string.nc_reply); @@ -422,22 +480,7 @@ public class NotificationWorker extends Worker { .build(); // Build a PendingIntent for the reply action - Intent actualIntent = new Intent(context, DirectReplyReceiver.class); - - // 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. - actualIntent.putExtra(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), Objects.requireNonNull(signatureVerification.getUserEntity()).getId()); - actualIntent.putExtra(BundleKeys.INSTANCE.getKEY_SYSTEM_NOTIFICATION_ID(), systemNotificationId); - actualIntent.putExtra(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), decryptedPushMessage.getId()); - - int intentFlag; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - intentFlag = PendingIntent.FLAG_MUTABLE|PendingIntent.FLAG_UPDATE_CURRENT; - } else { - intentFlag = PendingIntent.FLAG_UPDATE_CURRENT; - } - PendingIntent replyPendingIntent = - PendingIntent.getBroadcast(context, systemNotificationId, actualIntent, intentFlag); + PendingIntent replyPendingIntent = buildIntentForAction(DirectReplyReceiver.class, systemNotificationId, 0); NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(R.drawable.ic_reply, replyLabel, replyPendingIntent) @@ -458,16 +501,28 @@ public class NotificationWorker extends Worker { MessagingStyle newStyle = new MessagingStyle(person); newStyle.setConversationTitle(decryptedPushMessage.getSubject()); - newStyle.setGroupConversation(!conversationType.equals("one2one")); + newStyle.setGroupConversation(!"one2one".equals(conversationType)); if (style != null) { - style.getMessages().forEach(message -> newStyle.addMessage(new MessagingStyle.Message(message.getText(), message.getTimestamp(), message.getPerson()))); + style.getMessages().forEach(message -> newStyle.addMessage( + new MessagingStyle.Message(message.getText(), + message.getTimestamp(), + message.getPerson()))); } newStyle.addMessage(decryptedPushMessage.getText(), decryptedPushMessage.getTimestamp(), person); return newStyle; } + private int parseMessageId(@NonNull String objectId) { + String[] objectIdParts = objectId.split("/"); + if (objectIdParts.length < 2) { + throw new NumberFormatException("Invalid objectId, doesn't contain at least one '/'"); + } else { + return Integer.parseInt(objectIdParts[1]); + } + } + private void sendNotification(int notificationId, Notification notification) { NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); notificationManager.notify(notificationId, notification); @@ -478,7 +533,7 @@ public class NotificationWorker extends Worker { return; } - if (!notification.category.equals(Notification.CATEGORY_CALL) || !muteCall) { + if (!Notification.CATEGORY_CALL.equals(notification.category) || !muteCall) { Uri soundUri = NotificationUtils.INSTANCE.getMessageRingtoneUri(context, appPreferences); if (soundUri != null && !ApplicationWideCurrentRoomHolder.getInstance().isInCall() && (DoNotDisturbUtils.INSTANCE.shouldPlaySound() || importantConversation)) { @@ -536,12 +591,20 @@ public class NotificationWorker extends Worker { decryptedPushMessage.setTimestamp(System.currentTimeMillis()); if (decryptedPushMessage.getDelete()) { - NotificationUtils.INSTANCE.cancelExistingNotificationWithId(context, signatureVerification.getUserEntity(), decryptedPushMessage.getNotificationId()); + NotificationUtils.INSTANCE.cancelExistingNotificationWithId( + context, + signatureVerification.getUserEntity(), + decryptedPushMessage.getNotificationId()); } else if (decryptedPushMessage.getDeleteAll()) { - NotificationUtils.INSTANCE.cancelAllNotificationsForAccount(context, signatureVerification.getUserEntity()); + NotificationUtils.INSTANCE.cancelAllNotificationsForAccount( + context, + signatureVerification.getUserEntity()); } else if (decryptedPushMessage.getDeleteMultiple()) { for (long notificationId : decryptedPushMessage.getNotificationIds()) { - NotificationUtils.INSTANCE.cancelExistingNotificationWithId(context, signatureVerification.getUserEntity(), notificationId); + NotificationUtils.INSTANCE.cancelExistingNotificationWithId( + context, + signatureVerification.getUserEntity(), + notificationId); } } else { credentials = ApiUtils.getCredentials(signatureVerification.getUserEntity().getUsername(), @@ -550,14 +613,14 @@ public class NotificationWorker extends Worker { ncApi = retrofit.newBuilder().client(okHttpClient.newBuilder().cookieJar(new JavaNetCookieJar(new CookieManager())).build()).build().create(NcApi.class); - boolean shouldShowNotification = decryptedPushMessage.getApp().equals("spreed"); + boolean shouldShowNotification = "spreed".equals(decryptedPushMessage.getApp()); if (shouldShowNotification) { Intent intent; Bundle bundle = new Bundle(); - boolean startACall = decryptedPushMessage.getType().equals("call"); + boolean startACall = "call".equals(decryptedPushMessage.getType()); if (startACall) { intent = new Intent(context, CallActivity.class); } else { @@ -568,7 +631,8 @@ public class NotificationWorker extends Worker { bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), decryptedPushMessage.getId()); - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY(), signatureVerification.getUserEntity()); + bundle.putParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY(), + signatureVerification.getUserEntity()); bundle.putBoolean(BundleKeys.INSTANCE.getKEY_FROM_NOTIFICATION_START_CALL(), startACall); diff --git a/app/src/main/java/com/nextcloud/talk/models/json/push/DecryptedPushMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/push/DecryptedPushMessage.kt index a992f8bda..001305b51 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/push/DecryptedPushMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/push/DecryptedPushMessage.kt @@ -62,10 +62,13 @@ data class DecryptedPushMessage( var text: String?, @JsonIgnore - var timestamp: Long + var timestamp: Long, + + @JsonIgnore + var objectId: String? ) : Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' - constructor() : this(null, null, "", null, 0, null, false, false, false, null, null, 0) + constructor() : this(null, null, "", null, 0, null, false, false, false, null, null, 0, null) override fun equals(other: Any?): Boolean { if (this === other) return true @@ -88,6 +91,7 @@ data class DecryptedPushMessage( if (notificationUser != other.notificationUser) return false if (text != other.text) return false if (timestamp != other.timestamp) return false + if (objectId != other.objectId) return false return true } @@ -105,6 +109,7 @@ data class DecryptedPushMessage( result = 31 * result + (notificationUser?.hashCode() ?: 0) result = 31 * result + (text?.hashCode() ?: 0) result = 31 * result + (timestamp?.hashCode() ?: 0) + result = 31 * result + (objectId?.hashCode() ?: 0) return result } } diff --git a/app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt new file mode 100644 index 000000000..46095f852 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt @@ -0,0 +1,125 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * @author Dariusz Olszewski + * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de> + * Copyright (C) 2022 Dariusz Olszewski <starypatyk@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package com.nextcloud.talk.receivers + +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import autodagger.AutoInjector +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.utils.ApiUtils +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_ROOM_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID +import com.nextcloud.talk.utils.database.user.UserUtils +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 MarkAsReadReceiver : BroadcastReceiver() { + + @Inject + lateinit var userUtils: UserUtils + + @Inject + lateinit var ncApi: NcApi + + lateinit var context: Context + lateinit var currentUser: UserEntity + private var systemNotificationId: Int? = null + private var roomToken: String? = null + private var messageId: Int = 0 + + 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) + roomToken = intent.getStringExtra(KEY_ROOM_TOKEN) + messageId = intent.getIntExtra(KEY_MESSAGE_ID, 0) + + val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, userUtils.currentUser!!.id) + currentUser = userUtils.getUserWithId(id) + + markAsRead() + } + + private fun markAsRead() { + val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token) + val apiVersion = ApiUtils.getChatApiVersion(currentUser, intArrayOf(1)) + val url = ApiUtils.getUrlForSetChatReadMarker( + apiVersion, + currentUser.baseUrl, + roomToken + ) + + ncApi.setChatReadMarker(credentials, url, messageId) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer<GenericOverall> { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + @RequiresApi(Build.VERSION_CODES.N) + override fun onNext(genericOverall: GenericOverall) { + cancelNotification(systemNotificationId!!) + } + + @RequiresApi(Build.VERSION_CODES.N) + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to set chat read marker", e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun cancelNotification(notificationId: Int) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(notificationId) + } + + companion object { + const val TAG = "MarkAsReadReceiver" + } +} diff --git a/scripts/analysis/findbugs-results.txt b/scripts/analysis/findbugs-results.txt index 09df92759..05b9b66ff 100644 --- a/scripts/analysis/findbugs-results.txt +++ b/scripts/analysis/findbugs-results.txt @@ -1 +1 @@ -166 \ No newline at end of file +162 \ No newline at end of file