diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9e52ddd26..e6e3536c0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -177,6 +177,8 @@ + + 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 86c67b5ee..da1b8d074 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java @@ -37,15 +37,6 @@ import android.util.Base64; import android.util.Log; import com.bluelinelabs.logansquare.LoganSquare; -import com.facebook.common.executors.UiThreadImmediateExecutorService; -import com.facebook.common.references.CloseableReference; -import com.facebook.datasource.DataSource; -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.imagepipeline.core.ImagePipeline; -import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; -import com.facebook.imagepipeline.image.CloseableImage; -import com.facebook.imagepipeline.postprocessors.RoundAsCirclePostprocessor; -import com.facebook.imagepipeline.request.ImageRequest; import com.nextcloud.talk.R; import com.nextcloud.talk.activities.CallActivity; import com.nextcloud.talk.activities.MainActivity; @@ -60,8 +51,8 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall; 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.utils.ApiUtils; -import com.nextcloud.talk.utils.DisplayUtils; import com.nextcloud.talk.utils.DoNotDisturbUtils; import com.nextcloud.talk.utils.NotificationUtils; import com.nextcloud.talk.utils.PushUtils; @@ -87,10 +78,12 @@ import javax.inject.Inject; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationCompat.MessagingStyle; import androidx.core.app.NotificationManagerCompat; import androidx.core.app.Person; -import androidx.core.graphics.drawable.IconCompat; +import androidx.core.app.RemoteInput; import androidx.emoji.text.EmojiCompat; import androidx.work.Data; import androidx.work.Worker; @@ -299,10 +292,7 @@ public class NotificationWorker extends Worker { } } - intent.setAction(Long.toString(System.currentTimeMillis())); - - PendingIntent pendingIntent = PendingIntent.getActivity(context, - 0, intent, 0); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); Uri uri = Uri.parse(signatureVerification.getUserEntity().getBaseUrl()); String baseUrl = uri.getHost(); @@ -350,111 +340,118 @@ public class NotificationWorker extends Worker { notificationBuilder.setContentIntent(pendingIntent); - - CRC32 crc32 = new CRC32(); - String groupName = signatureVerification.getUserEntity().getId() + "@" + decryptedPushMessage.getId(); - crc32.update(groupName.getBytes()); - notificationBuilder.setGroup(Long.toString(crc32.getValue())); - - // notificationId - crc32 = new CRC32(); - String stringForCrc = String.valueOf(System.currentTimeMillis()); - crc32.update(stringForCrc.getBytes()); + notificationBuilder.setGroup(Long.toString(calculateCRC32(groupName))); StatusBarNotification activeStatusBarNotification = NotificationUtils.INSTANCE.findNotificationForRoom(context, signatureVerification.getUserEntity(), decryptedPushMessage.getId()); - int notificationId; - + // 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. + int systemNotificationId; if (activeStatusBarNotification != null) { - notificationId = activeStatusBarNotification.getId(); + systemNotificationId = activeStatusBarNotification.getId(); } else { - notificationId = (int) crc32.getValue(); + systemNotificationId = (int) calculateCRC32(String.valueOf(System.currentTimeMillis())); } - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N && decryptedPushMessage.getNotificationUser() != null && decryptedPushMessage.getType().equals("chat")) { - NotificationCompat.MessagingStyle style = null; - if (activeStatusBarNotification != null) { - Notification activeNotification = activeStatusBarNotification.getNotification(); - style = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(activeNotification); - } - - Person.Builder person = - new Person.Builder().setKey(signatureVerification.getUserEntity().getId() + - "@" + decryptedPushMessage.getNotificationUser().getId()).setName(EmojiCompat.get().process(decryptedPushMessage.getNotificationUser().getName())).setBot(decryptedPushMessage.getNotificationUser().getType().equals("bot")); - - notificationBuilder.setOnlyAlertOnce(true); - - if (decryptedPushMessage.getNotificationUser().getType().equals("user") || decryptedPushMessage.getNotificationUser().getType().equals("guest")) { - String avatarUrl = ApiUtils.getUrlForAvatar(signatureVerification.getUserEntity().getBaseUrl(), - decryptedPushMessage.getNotificationUser().getId(), false); - - if (decryptedPushMessage.getNotificationUser().getType().equals("guest")) { - avatarUrl = ApiUtils.getUrlForGuestAvatar(signatureVerification.getUserEntity().getBaseUrl(), - decryptedPushMessage.getNotificationUser().getName(), - false); - } - - ImageRequest imageRequest = - DisplayUtils.getImageRequestForUrl(avatarUrl, null); - ImagePipeline imagePipeline = Fresco.getImagePipeline(); - DataSource> dataSource = imagePipeline.fetchDecodedImage(imageRequest, context); - - NotificationCompat.MessagingStyle finalStyle = style; - dataSource.subscribe( - new BaseBitmapDataSubscriber() { - @Override - protected void onNewResultImpl(Bitmap bitmap) { - if (bitmap != null) { - new RoundAsCirclePostprocessor(true).process(bitmap); - person.setIcon(IconCompat.createWithBitmap(bitmap)); - notificationBuilder.setStyle(getStyle(person.build(), - finalStyle)); - sendNotificationWithId(notificationId, notificationBuilder.build()); - - } - } - - @Override - protected void onFailureImpl(DataSource> dataSource) { - notificationBuilder.setStyle(getStyle(person.build(), finalStyle)); - sendNotificationWithId(notificationId, notificationBuilder.build()); - } - }, - UiThreadImmediateExecutorService.getInstance()); - } else { - notificationBuilder.setStyle(getStyle(person.build(), style)); - sendNotificationWithId(notificationId, notificationBuilder.build()); - } - } else { - sendNotificationWithId(notificationId, notificationBuilder.build()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && + CHAT.equals(decryptedPushMessage.getType()) && + decryptedPushMessage.getNotificationUser() != null) { + prepareChatNotification(notificationBuilder, activeStatusBarNotification, systemNotificationId); } + sendNotification(systemNotificationId, notificationBuilder.build()); } - private NotificationCompat.MessagingStyle getStyle(Person person, @Nullable NotificationCompat.MessagingStyle style) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - NotificationCompat.MessagingStyle newStyle = - new NotificationCompat.MessagingStyle(person); - - newStyle.setConversationTitle(decryptedPushMessage.getSubject()); - newStyle.setGroupConversation(!conversationType.equals("one2one")); - - if (style != null) { - style.getMessages().forEach(message -> newStyle.addMessage(new NotificationCompat.MessagingStyle.Message(message.getText(), message.getTimestamp(), message.getPerson()))); - } - - newStyle.addMessage(decryptedPushMessage.getText(), decryptedPushMessage.getTimestamp(), person); - return newStyle; - } - - // we'll never come here - return style; + private long calculateCRC32(String s) { + CRC32 crc32 = new CRC32(); + crc32.update(s.getBytes()); + return crc32.getValue(); } - private void sendNotificationWithId(int notificationId, Notification notification) { + @RequiresApi(api = Build.VERSION_CODES.N) + private void prepareChatNotification(NotificationCompat.Builder notificationBuilder, + StatusBarNotification activeStatusBarNotification, + int systemNotificationId) { + + final NotificationUser notificationUser = decryptedPushMessage.getNotificationUser(); + final String userType = notificationUser.getType(); + + MessagingStyle style = null; + if (activeStatusBarNotification != null) { + style = MessagingStyle.extractMessagingStyleFromNotification(activeStatusBarNotification.getNotification()); + } + + Person.Builder person = + new Person.Builder() + .setKey(signatureVerification.getUserEntity().getId() + "@" + notificationUser.getId()) + .setName(EmojiCompat.get().process(notificationUser.getName())) + .setBot("bot".equals(userType)); + + notificationBuilder.setOnlyAlertOnce(true); + addReplyAction(notificationBuilder, systemNotificationId); + + if ("user".equals(userType) || "guest".equals(userType)) { + String baseUrl = signatureVerification.getUserEntity().getBaseUrl(); + String avatarUrl = "user".equals(userType) ? + ApiUtils.getUrlForAvatar(baseUrl, notificationUser.getId(), false) : + ApiUtils.getUrlForGuestAvatar(baseUrl, notificationUser.getName(), false); + person.setIcon(NotificationUtils.INSTANCE.loadAvatarSync(avatarUrl)); + } + + notificationBuilder.setStyle(getStyle(person.build(), style)); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private void addReplyAction(NotificationCompat.Builder notificationBuilder, int systemNotificationId) { + String replyLabel = context.getResources().getString(R.string.nc_reply); + + RemoteInput remoteInput = new RemoteInput.Builder(NotificationUtils.KEY_DIRECT_REPLY) + .setLabel(replyLabel) + .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_SYSTEM_NOTIFICATION_ID(), systemNotificationId); + actualIntent.putExtra(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), decryptedPushMessage.getId()); + PendingIntent replyPendingIntent = + PendingIntent.getBroadcast(context, systemNotificationId, actualIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.Action replyAction = + new NotificationCompat.Action.Builder(R.drawable.ic_reply, replyLabel, replyPendingIntent) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) + .setShowsUserInterface(false) + // Allows system to generate replies by context of conversation. + // https://developer.android.com/reference/androidx/core/app/NotificationCompat.Action.Builder#setAllowGeneratedReplies(boolean) + // Good question is - do we really want it? + .setAllowGeneratedReplies(true) + .addRemoteInput(remoteInput) + .build(); + + notificationBuilder.addAction(replyAction); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private MessagingStyle getStyle(Person person, @Nullable MessagingStyle style) { + MessagingStyle newStyle = new MessagingStyle(person); + + newStyle.setConversationTitle(decryptedPushMessage.getSubject()); + newStyle.setGroupConversation(!conversationType.equals("one2one")); + + if (style != null) { + 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 void sendNotification(int notificationId, Notification notification) { NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); notificationManager.notify(notificationId, notification); @@ -465,8 +462,7 @@ public class NotificationWorker extends Worker { } if (!notification.category.equals(Notification.CATEGORY_CALL) || !muteCall) { - Uri soundUri = NotificationUtils.INSTANCE.getMessageRingtoneUri(getApplicationContext(), - appPreferences); + Uri soundUri = NotificationUtils.INSTANCE.getMessageRingtoneUri(context, appPreferences); if (soundUri != null && !ApplicationWideCurrentRoomHolder.getInstance().isInCall() && (DoNotDisturbUtils.INSTANCE.shouldPlaySound() || importantConversation)) { AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder().setContentType diff --git a/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt new file mode 100644 index 000000000..26caea024 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt @@ -0,0 +1,157 @@ +/* + * Nextcloud Talk application + * + * @author Dariusz Olszewski + * Copyright (C) 2022 Dariusz Olszewski + * + * 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.Notification +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.Person +import androidx.core.app.RemoteInput +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.NotificationUtils +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 DirectReplyReceiver : BroadcastReceiver() { + + @Inject + @JvmField + var userUtils: UserUtils? = null + + @Inject + @JvmField + var ncApi: NcApi? = null + + lateinit var context: Context + lateinit var currentUser: UserEntity + private var systemNotificationId: Int? = null + private var roomToken: String? = null + private var replyMessage: CharSequence? = null + + init { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + } + + override fun onReceive(receiveContext: Context, intent: Intent?) { + context = receiveContext + currentUser = userUtils!!.currentUser!! + + // 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) + + replyMessage = getMessageText(intent) + sendDirectReply() + } + + private fun getMessageText(intent: Intent): CharSequence? { + return RemoteInput.getResultsFromIntent(intent)?.getCharSequence(NotificationUtils.KEY_DIRECT_REPLY) + } + + private fun sendDirectReply() { + val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token) + val apiVersion = ApiUtils.getChatApiVersion(currentUser, intArrayOf(1)) + val url = ApiUtils.getUrlForChat(apiVersion, currentUser.baseUrl, roomToken) + + ncApi!!.sendChatMessage(credentials, url, replyMessage, currentUser.displayName, null) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + @RequiresApi(Build.VERSION_CODES.N) + override fun onNext(genericOverall: GenericOverall) { + confirmReplySent() + } + + override fun onError(e: Throwable) { + // TODO - inform the user that sending of the reply failed + // unused atm + } + + override fun onComplete() { + // unused atm + } + }) + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun findActiveNotification(notificationId: Int): Notification? { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + return notificationManager.activeNotifications.find { it.id == notificationId }?.notification + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun confirmReplySent() { + // Implementation inspired by the SO question and article below: + // https://stackoverflow.com/questions/51549456/android-o-notification-for-direct-reply-message + // https://medium.com/@sidorovroman3/android-how-to-use-messagingstyle-for-notifications-without-caching-messages-c414ef2b816c + // + // Tries to follow "Best practices for messaging apps" described here: + // https://developer.android.com/training/notify-user/build-notification#messaging-best-practices + + // Find the original (active) notification + val previousNotification = findActiveNotification(systemNotificationId!!) ?: return + + // Recreate builder based on the active notification + val previousBuilder = NotificationCompat.Builder(context, previousNotification) + + // Extract MessagingStyle from the active notification + val previousStyle = NotificationCompat.MessagingStyle + .extractMessagingStyleFromNotification(previousNotification) + + // Add reply + val avatarUrl = ApiUtils.getUrlForAvatar(currentUser.baseUrl, currentUser.userId, false) + val me = Person.Builder() + .setName(currentUser.displayName) + .setIcon(NotificationUtils.loadAvatarSync(avatarUrl)) + .build() + val message = NotificationCompat.MessagingStyle.Message(replyMessage, System.currentTimeMillis(), me) + previousStyle?.addMessage(message) + + // Set the updated style + previousBuilder.setStyle(previousStyle) + + // Update the active notification. + NotificationManagerCompat.from(context).notify(systemNotificationId!!, previousBuilder.build()) + } +} 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 61941f251..a73a76179 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt @@ -32,7 +32,13 @@ import android.net.Uri import android.os.Build import android.service.notification.StatusBarNotification import android.text.TextUtils +import androidx.core.graphics.drawable.IconCompat import com.bluelinelabs.logansquare.LoganSquare +import com.facebook.common.references.CloseableReference +import com.facebook.datasource.DataSources +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.imagepipeline.image.CloseableBitmap +import com.facebook.imagepipeline.postprocessors.RoundAsCirclePostprocessor import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.R import com.nextcloud.talk.models.RingtoneSettings @@ -61,6 +67,9 @@ object NotificationUtils { const val DEFAULT_MESSAGE_RINGTONE_URI = "android.resource://" + BuildConfig.APPLICATION_ID + "/raw/librem_by_feandesign_message" + // RemoteInput key - used for replies sent directly from notification + const val KEY_DIRECT_REPLY = "key_direct_reply" + @TargetApi(Build.VERSION_CODES.O) private fun createNotificationChannel( context: Context, @@ -178,45 +187,46 @@ object NotificationUtils { return null } - fun cancelAllNotificationsForAccount(context: Context?, conversationUser: UserEntity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && conversationUser.id != -1L && context != null) { + private inline fun scanNotifications( + context: Context?, + conversationUser: UserEntity, + callback: ( + notificationManager: NotificationManager, + statusBarNotification: StatusBarNotification, + notification: Notification + ) -> Unit + ) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || conversationUser.id == -1L || context == null) { + return + } - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val statusBarNotifications = notificationManager.activeNotifications - var notification: Notification? - for (statusBarNotification in statusBarNotifications) { - notification = statusBarNotification.notification + val statusBarNotifications = notificationManager.activeNotifications + var notification: Notification? + for (statusBarNotification in statusBarNotifications) { + notification = statusBarNotification.notification - if (notification != null && !notification.extras.isEmpty) { - if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID)) { - notificationManager.cancel(statusBarNotification.id) - } - } + if ( + notification != null && + !notification.extras.isEmpty && + conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) + ) { + callback(notificationManager, statusBarNotification, notification) } } } + fun cancelAllNotificationsForAccount(context: Context?, conversationUser: UserEntity) { + scanNotifications(context, conversationUser) { notificationManager, statusBarNotification, _ -> + notificationManager.cancel(statusBarNotification.id) + } + } + fun cancelExistingNotificationWithId(context: Context?, conversationUser: UserEntity, notificationId: Long?) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && conversationUser.id != -1L && - context != null - ) { - - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - val statusBarNotifications = notificationManager.activeNotifications - var notification: Notification? - for (statusBarNotification in statusBarNotifications) { - notification = statusBarNotification.notification - - if (notification != null && !notification.extras.isEmpty) { - if ( - conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && - notificationId == notification.extras.getLong(BundleKeys.KEY_NOTIFICATION_ID) - ) { - notificationManager.cancel(statusBarNotification.id) - } - } + scanNotifications(context, conversationUser) { notificationManager, statusBarNotification, notification -> + if (notificationId == notification.extras.getLong(BundleKeys.KEY_NOTIFICATION_ID)) { + notificationManager.cancel(statusBarNotification.id) } } } @@ -226,28 +236,11 @@ object NotificationUtils { conversationUser: UserEntity, roomTokenOrId: String ): StatusBarNotification? { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && conversationUser.id != -1L && - context != null - ) { - - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - val statusBarNotifications = notificationManager.activeNotifications - var notification: Notification? - for (statusBarNotification in statusBarNotifications) { - notification = statusBarNotification.notification - - if (notification != null && !notification.extras.isEmpty) { - if ( - conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && - roomTokenOrId == statusBarNotification.notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN) - ) { - return statusBarNotification - } - } + scanNotifications(context, conversationUser) { _, statusBarNotification, notification -> + if (roomTokenOrId == notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN)) { + return statusBarNotification } } - return null } @@ -256,26 +249,9 @@ object NotificationUtils { conversationUser: UserEntity, roomTokenOrId: String ) { - if ( - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && - conversationUser.id != -1L && - context != null - ) { - - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - val statusBarNotifications = notificationManager.activeNotifications - var notification: Notification? - for (statusBarNotification in statusBarNotifications) { - notification = statusBarNotification.notification - - if (notification != null && !notification.extras.isEmpty) { - if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && - roomTokenOrId == statusBarNotification.notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN) - ) { - notificationManager.cancel(statusBarNotification.id) - } - } + scanNotifications(context, conversationUser) { notificationManager, statusBarNotification, notification -> + if (roomTokenOrId == notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN)) { + notificationManager.cancel(statusBarNotification.id) } } } @@ -294,15 +270,15 @@ object NotificationUtils { // Notification channel will not be available when starting the application for the first time. // Ringtone uris are required to register the notification channels -> get uri from preferences. } - if (TextUtils.isEmpty(ringtonePreferencesString)) { - return Uri.parse(defaultRingtoneUri) + return if (TextUtils.isEmpty(ringtonePreferencesString)) { + Uri.parse(defaultRingtoneUri) } else { try { val ringtoneSettings = LoganSquare.parse(ringtonePreferencesString, RingtoneSettings::class.java) - return ringtoneSettings.ringtoneUri + ringtoneSettings.ringtoneUri } catch (exception: IOException) { - return Uri.parse(defaultRingtoneUri) + Uri.parse(defaultRingtoneUri) } } } @@ -327,6 +303,31 @@ object NotificationUtils { ) } + /* + * Load user avatar synchronously. + * Inspired by: + * https://frescolib.org/docs/using-image-pipeline.html + * https://github.com/facebook/fresco/issues/830 + * https://localcoder.org/using-facebooks-fresco-to-load-a-bitmap + */ + fun loadAvatarSync(avatarUrl: String): IconCompat? { + // TODO - how to handle errors here? + var avatarIcon: IconCompat? = null + val imageRequest = DisplayUtils.getImageRequestForUrl(avatarUrl, null) + val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, null) + val closeableImageRef = DataSources.waitForFinalResult(dataSource) as CloseableReference? + val bitmap = closeableImageRef?.get()?.underlyingBitmap + if (bitmap != null) { + // According to Fresco documentation a copy of the bitmap should be made before closing the references. + // However, it seems to work without making a copy... ;-) + RoundAsCirclePostprocessor(true).process(bitmap) + avatarIcon = IconCompat.createWithBitmap(bitmap) + } + CloseableReference.closeSafely(closeableImageRef) + dataSource.close() + return avatarIcon + } + private data class Channel( val id: String, val name: String, 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 ff862156b..58d60e445 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 @@ -71,4 +71,5 @@ object BundleKeys { val KEY_FORWARD_MSG_FLAG = "KEY_FORWARD_MSG_FLAG" val KEY_FORWARD_MSG_TEXT = "KEY_FORWARD_MSG_TEXT" val KEY_FORWARD_HIDE_SOURCE_ROOM = "KEY_FORWARD_HIDE_SOURCE_ROOM" + val KEY_SYSTEM_NOTIFICATION_ID = "KEY_SYSTEM_NOTIFICATION_ID" } diff --git a/scripts/analysis/findbugs-results.txt b/scripts/analysis/findbugs-results.txt index 7b89b2202..6b3ed8d68 100644 --- a/scripts/analysis/findbugs-results.txt +++ b/scripts/analysis/findbugs-results.txt @@ -1 +1 @@ -406 \ No newline at end of file +400 \ No newline at end of file