Эх сурвалжийг харах

Merge pull request #1923 from nextcloud/feature/230/reply-from-notification

Reply from notification
Andy Scherzinger 3 жил өмнө
parent
commit
8f959c12dd

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

@@ -177,6 +177,8 @@
             </intent-filter>
             </intent-filter>
         </receiver>
         </receiver>
 
 
+        <receiver android:name=".receivers.DirectReplyReceiver" />
+
         <service
         <service
             android:name=".utils.SyncService"
             android:name=".utils.SyncService"
             android:exported="true">
             android:exported="true">

+ 91 - 95
app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java

@@ -37,15 +37,6 @@ import android.util.Base64;
 import android.util.Log;
 import android.util.Log;
 
 
 import com.bluelinelabs.logansquare.LoganSquare;
 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.R;
 import com.nextcloud.talk.activities.CallActivity;
 import com.nextcloud.talk.activities.CallActivity;
 import com.nextcloud.talk.activities.MainActivity;
 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.notifications.NotificationOverall;
 import com.nextcloud.talk.models.json.push.DecryptedPushMessage;
 import com.nextcloud.talk.models.json.push.DecryptedPushMessage;
 import com.nextcloud.talk.models.json.push.NotificationUser;
 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.ApiUtils;
-import com.nextcloud.talk.utils.DisplayUtils;
 import com.nextcloud.talk.utils.DoNotDisturbUtils;
 import com.nextcloud.talk.utils.DoNotDisturbUtils;
 import com.nextcloud.talk.utils.NotificationUtils;
 import com.nextcloud.talk.utils.NotificationUtils;
 import com.nextcloud.talk.utils.PushUtils;
 import com.nextcloud.talk.utils.PushUtils;
@@ -87,10 +78,12 @@ import javax.inject.Inject;
 
 
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.core.app.NotificationCompat;
 import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationCompat.MessagingStyle;
 import androidx.core.app.NotificationManagerCompat;
 import androidx.core.app.NotificationManagerCompat;
 import androidx.core.app.Person;
 import androidx.core.app.Person;
-import androidx.core.graphics.drawable.IconCompat;
+import androidx.core.app.RemoteInput;
 import androidx.emoji.text.EmojiCompat;
 import androidx.emoji.text.EmojiCompat;
 import androidx.work.Data;
 import androidx.work.Data;
 import androidx.work.Worker;
 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());
         Uri uri = Uri.parse(signatureVerification.getUserEntity().getBaseUrl());
         String baseUrl = uri.getHost();
         String baseUrl = uri.getHost();
@@ -350,111 +340,118 @@ public class NotificationWorker extends Worker {
 
 
         notificationBuilder.setContentIntent(pendingIntent);
         notificationBuilder.setContentIntent(pendingIntent);
 
 
-
-        CRC32 crc32 = new CRC32();
-
         String groupName = signatureVerification.getUserEntity().getId() + "@" + decryptedPushMessage.getId();
         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 =
         StatusBarNotification activeStatusBarNotification =
                 NotificationUtils.INSTANCE.findNotificationForRoom(context,
                 NotificationUtils.INSTANCE.findNotificationForRoom(context,
                         signatureVerification.getUserEntity(), decryptedPushMessage.getId());
                         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) {
         if (activeStatusBarNotification != null) {
-            notificationId = activeStatusBarNotification.getId();
+            systemNotificationId = activeStatusBarNotification.getId();
         } else {
         } 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"));
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
+            CHAT.equals(decryptedPushMessage.getType()) &&
+            decryptedPushMessage.getNotificationUser() != null) {
+            prepareChatNotification(notificationBuilder, activeStatusBarNotification, systemNotificationId);
+        }
 
 
-            notificationBuilder.setOnlyAlertOnce(true);
+        sendNotification(systemNotificationId, notificationBuilder.build());
+    }
 
 
-            if (decryptedPushMessage.getNotificationUser().getType().equals("user") || decryptedPushMessage.getNotificationUser().getType().equals("guest")) {
-                String avatarUrl = ApiUtils.getUrlForAvatar(signatureVerification.getUserEntity().getBaseUrl(),
-                                                            decryptedPushMessage.getNotificationUser().getId(), false);
+    private long calculateCRC32(String s) {
+        CRC32 crc32 = new CRC32();
+        crc32.update(s.getBytes());
+        return crc32.getValue();
+    }
 
 
-                if (decryptedPushMessage.getNotificationUser().getType().equals("guest")) {
-                    avatarUrl = ApiUtils.getUrlForGuestAvatar(signatureVerification.getUserEntity().getBaseUrl(),
-                                                              decryptedPushMessage.getNotificationUser().getName(),
-                                                              false);
-                }
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    private void prepareChatNotification(NotificationCompat.Builder notificationBuilder,
+                                         StatusBarNotification activeStatusBarNotification,
+                                         int systemNotificationId) {
 
 
-                ImageRequest imageRequest =
-                        DisplayUtils.getImageRequestForUrl(avatarUrl, null);
-                ImagePipeline imagePipeline = Fresco.getImagePipeline();
-                DataSource<CloseableReference<CloseableImage>> 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());
+        final NotificationUser notificationUser = decryptedPushMessage.getNotificationUser();
+        final String userType = notificationUser.getType();
 
 
-                                }
-                            }
+        MessagingStyle style = null;
+        if (activeStatusBarNotification != null) {
+            style = MessagingStyle.extractMessagingStyleFromNotification(activeStatusBarNotification.getNotification());
+        }
 
 
-                            @Override
-                            protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> 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());
+        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));
     }
     }
 
 
-    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);
+    @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);
+    }
 
 
-            newStyle.setConversationTitle(decryptedPushMessage.getSubject());
-            newStyle.setGroupConversation(!conversationType.equals("one2one"));
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    private MessagingStyle getStyle(Person person, @Nullable MessagingStyle style) {
+        MessagingStyle newStyle = new MessagingStyle(person);
 
 
-            if (style != null) {
-                style.getMessages().forEach(message -> newStyle.addMessage(new NotificationCompat.MessagingStyle.Message(message.getText(), message.getTimestamp(), message.getPerson())));
-            }
+        newStyle.setConversationTitle(decryptedPushMessage.getSubject());
+        newStyle.setGroupConversation(!conversationType.equals("one2one"));
 
 
-            newStyle.addMessage(decryptedPushMessage.getText(), decryptedPushMessage.getTimestamp(), person);
-            return newStyle;
+        if (style != null) {
+            style.getMessages().forEach(message -> newStyle.addMessage(new MessagingStyle.Message(message.getText(), message.getTimestamp(), message.getPerson())));
         }
         }
 
 
-        // we'll never come here
-        return style;
+        newStyle.addMessage(decryptedPushMessage.getText(), decryptedPushMessage.getTimestamp(), person);
+        return newStyle;
     }
     }
 
 
-    private void sendNotificationWithId(int notificationId, Notification notification) {
+    private void sendNotification(int notificationId, Notification notification) {
         NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
         NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
         notificationManager.notify(notificationId, notification);
         notificationManager.notify(notificationId, notification);
 
 
@@ -465,8 +462,7 @@ public class NotificationWorker extends Worker {
         }
         }
 
 
         if (!notification.category.equals(Notification.CATEGORY_CALL) || !muteCall) {
         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() &&
             if (soundUri != null && !ApplicationWideCurrentRoomHolder.getInstance().isInCall() &&
                     (DoNotDisturbUtils.INSTANCE.shouldPlaySound() || importantConversation)) {
                     (DoNotDisturbUtils.INSTANCE.shouldPlaySound() || importantConversation)) {
                 AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder().setContentType
                 AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder().setContentType

+ 157 - 0
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 <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.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<GenericOverall> {
+                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())
+    }
+}

+ 77 - 76
app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt

@@ -32,7 +32,13 @@ import android.net.Uri
 import android.os.Build
 import android.os.Build
 import android.service.notification.StatusBarNotification
 import android.service.notification.StatusBarNotification
 import android.text.TextUtils
 import android.text.TextUtils
+import androidx.core.graphics.drawable.IconCompat
 import com.bluelinelabs.logansquare.LoganSquare
 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.BuildConfig
 import com.nextcloud.talk.R
 import com.nextcloud.talk.R
 import com.nextcloud.talk.models.RingtoneSettings
 import com.nextcloud.talk.models.RingtoneSettings
@@ -61,6 +67,9 @@ object NotificationUtils {
     const val DEFAULT_MESSAGE_RINGTONE_URI =
     const val DEFAULT_MESSAGE_RINGTONE_URI =
         "android.resource://" + BuildConfig.APPLICATION_ID + "/raw/librem_by_feandesign_message"
         "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)
     @TargetApi(Build.VERSION_CODES.O)
     private fun createNotificationChannel(
     private fun createNotificationChannel(
         context: Context,
         context: Context,
@@ -178,45 +187,46 @@ object NotificationUtils {
         return null
         return null
     }
     }
 
 
-    fun cancelAllNotificationsForAccount(context: Context?, conversationUser: UserEntity) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && conversationUser.id != -1L && context != null) {
-
-            val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+    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 statusBarNotifications = notificationManager.activeNotifications
-            var notification: Notification?
-            for (statusBarNotification in statusBarNotifications) {
-                notification = statusBarNotification.notification
+        val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
 
 
-                if (notification != null && !notification.extras.isEmpty) {
-                    if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID)) {
-                        notificationManager.cancel(statusBarNotification.id)
-                    }
-                }
+        val statusBarNotifications = notificationManager.activeNotifications
+        var notification: Notification?
+        for (statusBarNotification in statusBarNotifications) {
+            notification = statusBarNotification.notification
+
+            if (
+                notification != null &&
+                !notification.extras.isEmpty &&
+                conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID)
+            ) {
+                callback(notificationManager, statusBarNotification, notification)
             }
             }
         }
         }
     }
     }
 
 
-    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
+    fun cancelAllNotificationsForAccount(context: Context?, conversationUser: UserEntity) {
+        scanNotifications(context, conversationUser) { notificationManager, statusBarNotification, _ ->
+            notificationManager.cancel(statusBarNotification.id)
+        }
+    }
 
 
-            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)
-                    }
-                }
+    fun cancelExistingNotificationWithId(context: Context?, conversationUser: UserEntity, notificationId: Long?) {
+        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,
         conversationUser: UserEntity,
         roomTokenOrId: String
         roomTokenOrId: String
     ): StatusBarNotification? {
     ): 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
         return null
     }
     }
 
 
@@ -256,26 +249,9 @@ object NotificationUtils {
         conversationUser: UserEntity,
         conversationUser: UserEntity,
         roomTokenOrId: String
         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.
             // 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.
             // 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 {
         } else {
             try {
             try {
                 val ringtoneSettings =
                 val ringtoneSettings =
                     LoganSquare.parse(ringtonePreferencesString, RingtoneSettings::class.java)
                     LoganSquare.parse(ringtonePreferencesString, RingtoneSettings::class.java)
-                return ringtoneSettings.ringtoneUri
+                ringtoneSettings.ringtoneUri
             } catch (exception: IOException) {
             } 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<CloseableBitmap>?
+        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(
     private data class Channel(
         val id: String,
         val id: String,
         val name: String,
         val name: String,

+ 1 - 0
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_FLAG = "KEY_FORWARD_MSG_FLAG"
     val KEY_FORWARD_MSG_TEXT = "KEY_FORWARD_MSG_TEXT"
     val KEY_FORWARD_MSG_TEXT = "KEY_FORWARD_MSG_TEXT"
     val KEY_FORWARD_HIDE_SOURCE_ROOM = "KEY_FORWARD_HIDE_SOURCE_ROOM"
     val KEY_FORWARD_HIDE_SOURCE_ROOM = "KEY_FORWARD_HIDE_SOURCE_ROOM"
+    val KEY_SYSTEM_NOTIFICATION_ID = "KEY_SYSTEM_NOTIFICATION_ID"
 }
 }

+ 1 - 1
scripts/analysis/findbugs-results.txt

@@ -1 +1 @@
-406
+400