link into custom notification settings from muc/contact details

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/services/NotificationService.java   |  59 
src/main/java/eu/siacs/conversations/services/ShortcutService.java       |  10 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |   2 
src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java   | 445 
src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java      | 335 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                | 635 
src/main/res/menu/contact_details.xml                                    |  44 
src/main/res/menu/muc_details.xml                                        |  39 
src/main/res/values/strings.xml                                          |   2 
9 files changed, 994 insertions(+), 577 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/services/NotificationService.java 🔗

@@ -78,6 +78,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -114,7 +115,7 @@ public class NotificationService {
     private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel";
     private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX =
             "incoming_calls_channel#";
-    private static final String MESSAGES_NOTIFICATION_CHANNEL = "messages";
+    public static final String MESSAGES_NOTIFICATION_CHANNEL = "messages";
 
     NotificationService(final XmppConnectionService service) {
         this.mXmppConnectionService = service;
@@ -229,25 +230,8 @@ public class NotificationService {
         missedCallsChannel.setGroup("calls");
         notificationManager.createNotificationChannel(missedCallsChannel);
 
-        final NotificationChannel messagesChannel =
-                new NotificationChannel(
-                        MESSAGES_NOTIFICATION_CHANNEL,
-                        c.getString(R.string.messages_channel_name),
-                        NotificationManager.IMPORTANCE_HIGH);
-        messagesChannel.setShowBadge(true);
-        messagesChannel.setSound(
-                RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
-                new AudioAttributes.Builder()
-                        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
-                        .setUsage(AudioAttributes.USAGE_NOTIFICATION)
-                        .build());
-        messagesChannel.setLightColor(LED_COLOR);
-        final int dat = 70;
-        final long[] pattern = {0, 3 * dat, dat, dat};
-        messagesChannel.setVibrationPattern(pattern);
-        messagesChannel.enableVibration(true);
-        messagesChannel.enableLights(true);
-        messagesChannel.setGroup("chats");
+        final var messagesChannel =
+                prepareMessagesChannel(mXmppConnectionService, MESSAGES_NOTIFICATION_CHANNEL);
         notificationManager.createNotificationChannel(messagesChannel);
         final NotificationChannel silentMessagesChannel =
                 new NotificationChannel(
@@ -278,6 +262,41 @@ public class NotificationService {
         notificationManager.createNotificationChannel(deliveryFailedChannel);
     }
 
+    @RequiresApi(api = Build.VERSION_CODES.R)
+    public static void createConversationChannel(
+            final Context context, final ShortcutInfoCompat shortcut) {
+        final var messagesChannel = prepareMessagesChannel(context, UUID.randomUUID().toString());
+        messagesChannel.setName(shortcut.getShortLabel());
+        messagesChannel.setConversationId(MESSAGES_NOTIFICATION_CHANNEL, shortcut.getId());
+        final var notificationManager = context.getSystemService(NotificationManager.class);
+        notificationManager.createNotificationChannel(messagesChannel);
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    private static NotificationChannel prepareMessagesChannel(
+            final Context context, final String id) {
+        final NotificationChannel messagesChannel =
+                new NotificationChannel(
+                        id,
+                        context.getString(R.string.messages_channel_name),
+                        NotificationManager.IMPORTANCE_HIGH);
+        messagesChannel.setShowBadge(true);
+        messagesChannel.setSound(
+                RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
+                new AudioAttributes.Builder()
+                        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+                        .setUsage(AudioAttributes.USAGE_NOTIFICATION)
+                        .build());
+        messagesChannel.setLightColor(LED_COLOR);
+        final int dat = 70;
+        final long[] pattern = {0, 3 * dat, dat, dat};
+        messagesChannel.setVibrationPattern(pattern);
+        messagesChannel.enableVibration(true);
+        messagesChannel.enableLights(true);
+        messagesChannel.setGroup("chats");
+        return messagesChannel;
+    }
+
     @RequiresApi(api = Build.VERSION_CODES.O)
     private static void createInitialIncomingCallChannelIfNecessary(final Context context) {
         final var currentIteration = getCurrentIncomingCallChannelIteration(context);

src/main/java/eu/siacs/conversations/services/ShortcutService.java 🔗

@@ -93,6 +93,12 @@ public class ShortcutService {
         }
     }
 
+    public ShortcutInfoCompat getShortcutInfo(final Contact contact) {
+        final var conversation = xmppConnectionService.find(contact);
+        final var uuid = conversation == null ? null : conversation.getUuid();
+        return getShortcutInfo(contact, uuid);
+    }
+
     public ShortcutInfoCompat getShortcutInfo(final Contact contact, final String conversation) {
         final ShortcutInfoCompat.Builder builder =
                 new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(contact))
@@ -199,9 +205,7 @@ public class ShortcutService {
     public Intent createShortcut(final Contact contact, final boolean legacy) {
         Intent intent;
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !legacy) {
-            final var conversation = xmppConnectionService.find(contact);
-            final var uuid = conversation == null ? null : conversation.getUuid();
-            final var shortcut = getShortcutInfo(contact, uuid);
+            final var shortcut = getShortcutInfo(contact);
             intent =
                     ShortcutManagerCompat.createShortcutResultIntent(
                             xmppConnectionService, shortcut);

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -2302,6 +2302,7 @@ public class XmppConnectionService extends Service {
                     Config.LOGTAG,
                     account.getJid().asBareJid() + ": pushing bookmark via Bookmarks 2");
             final Element item = mIqGenerator.publishBookmarkItem(bookmark);
+            Log.d(Config.LOGTAG, "publishing: " + item.toString());
             pushNodeAndEnforcePublishOptions(
                     account,
                     Namespace.BOOKMARKS2,
@@ -2873,6 +2874,7 @@ public class XmppConnectionService extends Service {
                                 existing.getMode() == Conversational.MODE_MULTI,
                                 null));
         this.conversations.add(existing);
+        // TODO push bookmark
         updateConversationUi();
         return existing;
     }

src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java 🔗

@@ -1,9 +1,13 @@
 package eu.siacs.conversations.ui;
 
+import static eu.siacs.conversations.entities.Bookmark.printableValue;
+import static eu.siacs.conversations.utils.StringUtils.changed;
+
 import android.app.Activity;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
+import android.os.Build;
 import android.os.Bundle;
 import android.text.Editable;
 import android.text.SpannableStringBuilder;
@@ -14,20 +18,14 @@ import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.Toast;
-
+import androidx.annotation.NonNull;
 import androidx.appcompat.app.AlertDialog;
 import androidx.databinding.DataBindingUtil;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import eu.siacs.conversations.Config;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.ActivityMucDetailsBinding;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Bookmark;
 import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Conversational;
 import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.entities.MucOptions.User;
 import eu.siacs.conversations.services.XmppConnectionService;
@@ -50,14 +48,19 @@ import eu.siacs.conversations.utils.StringUtils;
 import eu.siacs.conversations.utils.StylingHelper;
 import eu.siacs.conversations.utils.XmppUri;
 import eu.siacs.conversations.xmpp.Jid;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
 import me.drakeet.support.toast.ToastCompat;
 
-import static eu.siacs.conversations.entities.Bookmark.printableValue;
-import static eu.siacs.conversations.utils.StringUtils.changed;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-
-public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnConfigurationPushed, XmppConnectionService.OnRoomDestroy, TextWatcher, OnMediaLoaded {
+public class ConferenceDetailsActivity extends XmppActivity
+        implements OnConversationUpdate,
+                OnMucRosterUpdate,
+                XmppConnectionService.OnAffiliationChanged,
+                XmppConnectionService.OnConfigurationPushed,
+                XmppConnectionService.OnRoomDestroy,
+                TextWatcher,
+                OnMediaLoaded {
     public static final String ACTION_VIEW_MUC = "view_muc";
 
     private Conversation mConversation;
@@ -68,26 +71,25 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
 
     private boolean mAdvancedMode = false;
 
-    private final UiCallback<Conversation> renameCallback = new UiCallback<Conversation>() {
-        @Override
-        public void success(Conversation object) {
-            displayToast(getString(R.string.your_nick_has_been_changed));
-            runOnUiThread(() -> {
-                updateView();
-            });
-
-        }
-
-        @Override
-        public void error(final int errorCode, Conversation object) {
-            displayToast(getString(errorCode));
-        }
+    private final UiCallback<Conversation> renameCallback =
+            new UiCallback<Conversation>() {
+                @Override
+                public void success(Conversation object) {
+                    displayToast(getString(R.string.your_nick_has_been_changed));
+                    runOnUiThread(
+                            () -> {
+                                updateView();
+                            });
+                }
 
-        @Override
-        public void userInputRequired(PendingIntent pi, Conversation object) {
+                @Override
+                public void error(final int errorCode, Conversation object) {
+                    displayToast(getString(errorCode));
+                }
 
-        }
-    };
+                @Override
+                public void userInputRequired(PendingIntent pi, Conversation object) {}
+            };
 
     public static void open(final Activity activity, final Conversation conversation) {
         Intent intent = new Intent(activity, ConferenceDetailsActivity.class);
@@ -96,37 +98,45 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         activity.startActivity(intent);
     }
 
-    private final OnClickListener mNotifyStatusClickListener = new OnClickListener() {
-        @Override
-        public void onClick(View v) {
-            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this);
-            builder.setTitle(R.string.pref_notification_settings);
-            String[] choices = {
-                    getString(R.string.notify_on_all_messages),
-                    getString(R.string.notify_only_when_highlighted),
-                    getString(R.string.notify_never)
-            };
-            final AtomicInteger choice;
-            if (mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0) == Long.MAX_VALUE) {
-                choice = new AtomicInteger(2);
-            } else {
-                choice = new AtomicInteger(mConversation.alwaysNotify() ? 0 : 1);
-            }
-            builder.setSingleChoiceItems(choices, choice.get(), (dialog, which) -> choice.set(which));
-            builder.setNegativeButton(R.string.cancel, null);
-            builder.setPositiveButton(R.string.ok, (dialog, which) -> {
-                if (choice.get() == 2) {
-                    mConversation.setMutedTill(Long.MAX_VALUE);
-                } else {
-                    mConversation.setMutedTill(0);
-                    mConversation.setAttribute(Conversation.ATTRIBUTE_ALWAYS_NOTIFY, String.valueOf(choice.get() == 0));
+    private final OnClickListener mNotifyStatusClickListener =
+            new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    final MaterialAlertDialogBuilder builder =
+                            new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this);
+                    builder.setTitle(R.string.pref_notification_settings);
+                    String[] choices = {
+                        getString(R.string.notify_on_all_messages),
+                        getString(R.string.notify_only_when_highlighted),
+                        getString(R.string.notify_never)
+                    };
+                    final AtomicInteger choice;
+                    if (mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0)
+                            == Long.MAX_VALUE) {
+                        choice = new AtomicInteger(2);
+                    } else {
+                        choice = new AtomicInteger(mConversation.alwaysNotify() ? 0 : 1);
+                    }
+                    builder.setSingleChoiceItems(
+                            choices, choice.get(), (dialog, which) -> choice.set(which));
+                    builder.setNegativeButton(R.string.cancel, null);
+                    builder.setPositiveButton(
+                            R.string.ok,
+                            (dialog, which) -> {
+                                if (choice.get() == 2) {
+                                    mConversation.setMutedTill(Long.MAX_VALUE);
+                                } else {
+                                    mConversation.setMutedTill(0);
+                                    mConversation.setAttribute(
+                                            Conversation.ATTRIBUTE_ALWAYS_NOTIFY,
+                                            String.valueOf(choice.get() == 0));
+                                }
+                                xmppConnectionService.updateConversation(mConversation);
+                                updateView();
+                            });
+                    builder.create().show();
                 }
-                xmppConnectionService.updateConversation(mConversation);
-                updateView();
-            });
-            builder.create().show();
-        }
-    };
+            };
 
     private final OnClickListener mChangeConferenceSettings =
             new OnClickListener() {
@@ -185,37 +195,56 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         this.binding.changeConferenceButton.setOnClickListener(this.mChangeConferenceSettings);
         setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar());
-        this.binding.editNickButton.setOnClickListener(v -> quickEdit(mConversation.getMucOptions().getActualNick(),
-                R.string.nickname,
-                value -> {
-                    if (xmppConnectionService.renameInMuc(mConversation, value, renameCallback)) {
-                        return null;
-                    } else {
-                        return getString(R.string.invalid_muc_nick);
-                    }
-                }));
+        this.binding.editNickButton.setOnClickListener(
+                v ->
+                        quickEdit(
+                                mConversation.getMucOptions().getActualNick(),
+                                R.string.nickname,
+                                value -> {
+                                    if (xmppConnectionService.renameInMuc(
+                                            mConversation, value, renameCallback)) {
+                                        return null;
+                                    } else {
+                                        return getString(R.string.invalid_muc_nick);
+                                    }
+                                }));
         this.mAdvancedMode = getPreferences().getBoolean("advanced_muc_mode", false);
         this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
         this.binding.notificationStatusButton.setOnClickListener(this.mNotifyStatusClickListener);
-        this.binding.yourPhoto.setOnClickListener(v -> {
-            final MucOptions mucOptions = mConversation.getMucOptions();
-            if (!mucOptions.hasVCards()) {
-                Toast.makeText(this, R.string.host_does_not_support_group_chat_avatars, Toast.LENGTH_SHORT).show();
-                return;
-            }
-            if (!mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
-                Toast.makeText(this, R.string.only_the_owner_can_change_group_chat_avatar, Toast.LENGTH_SHORT).show();
-                return;
-            }
-            final Intent intent = new Intent(this, PublishGroupChatProfilePictureActivity.class);
-            intent.putExtra("uuid", mConversation.getUuid());
-            startActivity(intent);
-        });
-        this.binding.editMucNameButton.setContentDescription(getString(R.string.edit_name_and_topic));
+        this.binding.yourPhoto.setOnClickListener(
+                v -> {
+                    final MucOptions mucOptions = mConversation.getMucOptions();
+                    if (!mucOptions.hasVCards()) {
+                        Toast.makeText(
+                                        this,
+                                        R.string.host_does_not_support_group_chat_avatars,
+                                        Toast.LENGTH_SHORT)
+                                .show();
+                        return;
+                    }
+                    if (!mucOptions
+                            .getSelf()
+                            .getAffiliation()
+                            .ranks(MucOptions.Affiliation.OWNER)) {
+                        Toast.makeText(
+                                        this,
+                                        R.string.only_the_owner_can_change_group_chat_avatar,
+                                        Toast.LENGTH_SHORT)
+                                .show();
+                        return;
+                    }
+                    final Intent intent =
+                            new Intent(this, PublishGroupChatProfilePictureActivity.class);
+                    intent.putExtra("uuid", mConversation.getUuid());
+                    startActivity(intent);
+                });
+        this.binding.editMucNameButton.setContentDescription(
+                getString(R.string.edit_name_and_topic));
         this.binding.editMucNameButton.setOnClickListener(this::onMucEditButtonClicked);
         this.binding.mucEditTitle.addTextChangedListener(this);
         this.binding.mucEditSubject.addTextChangedListener(this);
-        this.binding.mucEditSubject.addTextChangedListener(new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject));
+        this.binding.mucEditSubject.addTextChangedListener(
+                new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject));
         this.mMediaAdapter = new MediaAdapter(this, R.dimen.media_size);
         this.mUserPreviewAdapter = new UserPreviewAdapter();
         this.binding.media.setAdapter(mMediaAdapter);
@@ -223,17 +252,19 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size);
         GridManager.setupLayoutManager(this, this.binding.users, R.dimen.media_size);
         this.binding.invite.setOnClickListener(v -> inviteToConversation(mConversation));
-        this.binding.showUsers.setOnClickListener(v -> {
-            Intent intent = new Intent(this, MucUsersActivity.class);
-            intent.putExtra("uuid", mConversation.getUuid());
-            startActivity(intent);
-        });
+        this.binding.showUsers.setOnClickListener(
+                v -> {
+                    Intent intent = new Intent(this, MucUsersActivity.class);
+                    intent.putExtra("uuid", mConversation.getUuid());
+                    startActivity(intent);
+                });
     }
 
     @Override
     public void onStart() {
         super.onStart();
-        binding.mediaWrapper.setVisibility(Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
+        binding.mediaWrapper.setVisibility(
+                Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
     }
 
     @Override
@@ -261,23 +292,43 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
                 this.mAdvancedMode = !menuItem.isChecked();
                 menuItem.setChecked(this.mAdvancedMode);
                 getPreferences().edit().putBoolean("advanced_muc_mode", mAdvancedMode).apply();
-                final boolean online = mConversation != null && mConversation.getMucOptions().online();
-                this.binding.mucInfoMore.setVisibility(this.mAdvancedMode && online ? View.VISIBLE : View.GONE);
+                final boolean online =
+                        mConversation != null && mConversation.getMucOptions().online();
+                this.binding.mucInfoMore.setVisibility(
+                        this.mAdvancedMode && online ? View.VISIBLE : View.GONE);
                 invalidateOptionsMenu();
                 updateView();
                 break;
+            case R.id.action_custom_notifications:
+                if (mConversation != null) {
+                    configureCustomNotifications(mConversation);
+                }
+                break;
         }
         return super.onOptionsItemSelected(menuItem);
     }
 
+    private void configureCustomNotifications(final Conversation conversation) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R
+                || conversation.getMode() != Conversational.MODE_MULTI) {
+            return;
+        }
+        final var shortcut =
+                xmppConnectionService
+                        .getShortcutService()
+                        .getShortcutInfo(conversation.getMucOptions());
+        configureCustomNotification(shortcut);
+    }
+
     @Override
-    public boolean onContextItemSelected(MenuItem item) {
+    public boolean onContextItemSelected(@NonNull final MenuItem item) {
         final User user = mUserPreviewAdapter.getSelectedUser();
         if (user == null) {
             Toast.makeText(this, R.string.unable_to_perform_this_action, Toast.LENGTH_SHORT).show();
             return true;
         }
-        if (!MucDetailsContextMenuHelper.onContextItemSelected(item, mUserPreviewAdapter.getSelectedUser(), this)) {
+        if (!MucDetailsContextMenuHelper.onContextItemSelected(
+                item, mUserPreviewAdapter.getSelectedUser(), this)) {
             return super.onContextItemSelected(item);
         }
         return true;
@@ -292,7 +343,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             this.binding.editMucNameButton.setContentDescription(getString(R.string.cancel));
             final String name = mucOptions.getName();
             this.binding.mucEditTitle.setText("");
-            final boolean owner = mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER);
+            final boolean owner =
+                    mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER);
             if (owner || printableValue(name)) {
                 this.binding.mucEditTitle.setVisibility(View.VISIBLE);
                 if (name != null) {
@@ -312,8 +364,14 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
                 this.binding.mucEditSubject.requestFocus();
             }
         } else {
-            String subject = this.binding.mucEditSubject.isEnabled() ? this.binding.mucEditSubject.getEditableText().toString().trim() : null;
-            String name = this.binding.mucEditTitle.isEnabled() ? this.binding.mucEditTitle.getEditableText().toString().trim() : null;
+            String subject =
+                    this.binding.mucEditSubject.isEnabled()
+                            ? this.binding.mucEditSubject.getEditableText().toString().trim()
+                            : null;
+            String name =
+                    this.binding.mucEditTitle.isEnabled()
+                            ? this.binding.mucEditTitle.getEditableText().toString().trim()
+                            : null;
             onMucInfoUpdated(subject, name);
             SoftKeyboardUtils.hideSoftKeyboard(this);
             hideEditor();
@@ -324,7 +382,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         this.binding.mucEditor.setVisibility(View.GONE);
         this.binding.mucDisplay.setVisibility(View.VISIBLE);
         this.binding.editMucNameButton.setImageResource(R.drawable.ic_edit_24dp);
-        this.binding.editMucNameButton.setContentDescription(getString(R.string.edit_name_and_topic));
+        this.binding.editMucNameButton.setContentDescription(
+                getString(R.string.edit_name_and_topic));
     }
 
     private void onMucInfoUpdated(String subject, String name) {
@@ -332,7 +391,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         if (mucOptions.canChangeSubject() && changed(mucOptions.getSubject(), subject)) {
             xmppConnectionService.pushSubjectToConference(mConversation, subject);
         }
-        if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER) && changed(mucOptions.getName(), name)) {
+        if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)
+                && changed(mucOptions.getName(), name)) {
             Bundle options = new Bundle();
             options.putString("muc#roomconfig_persistentroom", "1");
             options.putString("muc#roomconfig_roomname", StringUtils.nullOnEmpty(name));
@@ -340,12 +400,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         }
     }
 
-
     @Override
     protected String getShareableUri(boolean http) {
         if (mConversation != null) {
             if (http) {
-                return "https://conversations.im/j/" + XmppUri.lameUrlEncode(mConversation.getJid().asBareJid().toEscapedString());
+                return "https://conversations.im/j/"
+                        + XmppUri.lameUrlEncode(
+                                mConversation.getJid().asBareJid().toEscapedString());
             } else {
                 return "xmpp:" + mConversation.getJid().asBareJid() + "?join";
             }
@@ -364,7 +425,12 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             return true;
         }
         menuItemSaveBookmark.setVisible(mConversation.getBookmark() == null);
-        menuItemDestroyRoom.setVisible(mConversation.getMucOptions().getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER));
+        menuItemDestroyRoom.setVisible(
+                mConversation
+                        .getMucOptions()
+                        .getSelf()
+                        .getAffiliation()
+                        .ranks(MucOptions.Affiliation.OWNER));
         return true;
     }
 
@@ -377,32 +443,40 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         final MenuItem destroy = menu.findItem(R.id.action_destroy_room);
         destroy.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel);
         AccountUtils.showHideMenuItems(menu);
+        final MenuItem customNotifications = menu.findItem(R.id.action_custom_notifications);
+        customNotifications.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R);
         return super.onCreateOptionsMenu(menu);
     }
 
     @Override
-    public void onMediaLoaded(List<Attachment> attachments) {
-        runOnUiThread(() -> {
-            int limit = GridManager.getCurrentColumnCount(binding.media);
-            mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit, attachments.size())));
-            binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE);
-        });
-
+    public void onMediaLoaded(final List<Attachment> attachments) {
+        runOnUiThread(
+                () -> {
+                    final int limit = GridManager.getCurrentColumnCount(binding.media);
+                    mMediaAdapter.setAttachments(
+                            attachments.subList(0, Math.min(limit, attachments.size())));
+                    binding.mediaWrapper.setVisibility(
+                            attachments.isEmpty() ? View.GONE : View.VISIBLE);
+                });
     }
 
-
     protected void saveAsBookmark() {
-        xmppConnectionService.saveConversationAsBookmark(mConversation, mConversation.getMucOptions().getName());
+        xmppConnectionService.saveConversationAsBookmark(
+                mConversation, mConversation.getMucOptions().getName());
     }
 
     protected void destroyRoom() {
         final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
         final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel);
-        builder.setMessage(groupChat ? R.string.destroy_room_dialog : R.string.destroy_channel_dialog);
-        builder.setPositiveButton(R.string.ok, (dialog, which) -> {
-            xmppConnectionService.destroyRoom(mConversation, ConferenceDetailsActivity.this);
-        });
+        builder.setMessage(
+                groupChat ? R.string.destroy_room_dialog : R.string.destroy_channel_dialog);
+        builder.setPositiveButton(
+                R.string.ok,
+                (dialog, which) -> {
+                    xmppConnectionService.destroyRoom(
+                            mConversation, ConferenceDetailsActivity.this);
+                });
         builder.setNegativeButton(R.string.cancel, null);
         final AlertDialog dialog = builder.create();
         dialog.setCanceledOnTouchOutside(false);
@@ -424,7 +498,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
                 if (Compatibility.hasStoragePermission(this)) {
                     final int limit = GridManager.getCurrentColumnCount(this.binding.media);
                     xmppConnectionService.getAttachments(this.mConversation, limit, this);
-                    this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, mConversation));
+                    this.binding.showMedia.setOnClickListener(
+                            (v) -> MediaBrowserActivity.launch(this, mConversation));
                 }
                 updateView();
             }
@@ -448,15 +523,24 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         final MucOptions mucOptions = mConversation.getMucOptions();
         final User self = mucOptions.getSelf();
         final String account = mConversation.getAccount().getJid().asBareJid().toEscapedString();
-        setTitle(mucOptions.isPrivateAndNonAnonymous() ? R.string.action_muc_details : R.string.channel_details);
-        this.binding.editMucNameButton.setVisibility((self.getAffiliation().ranks(MucOptions.Affiliation.OWNER) || mucOptions.canChangeSubject()) ? View.VISIBLE : View.GONE);
+        setTitle(
+                mucOptions.isPrivateAndNonAnonymous()
+                        ? R.string.action_muc_details
+                        : R.string.channel_details);
+        this.binding.editMucNameButton.setVisibility(
+                (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)
+                                || mucOptions.canChangeSubject())
+                        ? View.VISIBLE
+                        : View.GONE);
         this.binding.detailsAccount.setText(getString(R.string.using_account, account));
         if (mConversation.isPrivateAndNonAnonymous()) {
-            this.binding.jid.setText(getString(R.string.hosted_on, mConversation.getJid().getDomain()));
+            this.binding.jid.setText(
+                    getString(R.string.hosted_on, mConversation.getJid().getDomain()));
         } else {
             this.binding.jid.setText(mConversation.getJid().asBareJid().toEscapedString());
         }
-        AvatarWorkerTask.loadAvatar(mConversation, binding.yourPhoto, R.dimen.avatar_on_details_screen_size);
+        AvatarWorkerTask.loadAvatar(
+                mConversation, binding.yourPhoto, R.dimen.avatar_on_details_screen_size);
         String roomName = mucOptions.getName();
         String subject = mucOptions.getSubject();
         final boolean hasTitle;
@@ -477,7 +561,12 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor());
             MyLinkify.addLinks(spannable, false);
             this.binding.mucSubject.setText(spannable);
-            this.binding.mucSubject.setTextAppearance( subject.length() > (hasTitle ? 128 : 196) ? com.google.android.material.R.style.TextAppearance_Material3_BodyMedium : com.google.android.material.R.style.TextAppearance_Material3_BodyLarge);
+            this.binding.mucSubject.setTextAppearance(
+                    subject.length() > (hasTitle ? 128 : 196)
+                            ? com.google.android.material.R.style
+                                    .TextAppearance_Material3_BodyMedium
+                            : com.google.android.material.R.style
+                                    .TextAppearance_Material3_BodyLarge);
             this.binding.mucSubject.setAutoLinkMask(0);
             this.binding.mucSubject.setVisibility(View.VISIBLE);
             this.binding.mucSubject.setMovementMethod(LinkMovementMethod.getInstance());
@@ -495,7 +584,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
                 this.binding.mucConferenceType.setText(MucConfiguration.describe(this, mucOptions));
             } else if (!mucOptions.isPrivateAndNonAnonymous() && mucOptions.nonanonymous()) {
                 this.binding.mucSettings.setVisibility(View.VISIBLE);
-                this.binding.mucConferenceType.setText(R.string.group_chat_will_make_your_jabber_id_public);
+                this.binding.mucConferenceType.setText(
+                        R.string.group_chat_will_make_your_jabber_id_public);
             } else {
                 this.binding.mucSettings.setVisibility(View.GONE);
             }
@@ -518,50 +608,64 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         final long mutedTill = mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0);
         if (mutedTill == Long.MAX_VALUE) {
             this.binding.notificationStatusText.setText(R.string.notify_never);
-            this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_off_24dp);
+            this.binding.notificationStatusButton.setImageResource(
+                    R.drawable.ic_notifications_off_24dp);
         } else if (System.currentTimeMillis() < mutedTill) {
             this.binding.notificationStatusText.setText(R.string.notify_paused);
-            this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_paused_24dp);
+            this.binding.notificationStatusButton.setImageResource(
+                    R.drawable.ic_notifications_paused_24dp);
         } else if (mConversation.alwaysNotify()) {
             this.binding.notificationStatusText.setText(R.string.notify_on_all_messages);
-            this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_24dp);
+            this.binding.notificationStatusButton.setImageResource(
+                    R.drawable.ic_notifications_24dp);
         } else {
             this.binding.notificationStatusText.setText(R.string.notify_only_when_highlighted);
-            this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_none_24dp);
+            this.binding.notificationStatusButton.setImageResource(
+                    R.drawable.ic_notifications_none_24dp);
         }
         final List<User> users = mucOptions.getUsers();
-        Collections.sort(users, (a, b) -> {
-            if (b.getAffiliation().outranks(a.getAffiliation())) {
-                return 1;
-            } else if (a.getAffiliation().outranks(b.getAffiliation())) {
-                return -1;
-            } else {
-                if (a.getAvatar() != null && b.getAvatar() == null) {
-                    return -1;
-                } else if (a.getAvatar() == null && b.getAvatar() != null) {
-                    return 1;
-                } else {
-                    return a.getComparableName().compareToIgnoreCase(b.getComparableName());
-                }
-            }
-        });
-        this.mUserPreviewAdapter.submitList(MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users)));
+        Collections.sort(
+                users,
+                (a, b) -> {
+                    if (b.getAffiliation().outranks(a.getAffiliation())) {
+                        return 1;
+                    } else if (a.getAffiliation().outranks(b.getAffiliation())) {
+                        return -1;
+                    } else {
+                        if (a.getAvatar() != null && b.getAvatar() == null) {
+                            return -1;
+                        } else if (a.getAvatar() == null && b.getAvatar() != null) {
+                            return 1;
+                        } else {
+                            return a.getComparableName().compareToIgnoreCase(b.getComparableName());
+                        }
+                    }
+                });
+        this.mUserPreviewAdapter.submitList(
+                MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users)));
         this.binding.invite.setVisibility(mucOptions.canInvite() ? View.VISIBLE : View.GONE);
         this.binding.showUsers.setVisibility(users.size() > 0 ? View.VISIBLE : View.GONE);
-        this.binding.showUsers.setText(getResources().getQuantityString(R.plurals.view_users, users.size(), users.size()));
-        this.binding.usersWrapper.setVisibility(users.size() > 0 || mucOptions.canInvite() ? View.VISIBLE : View.GONE);
+        this.binding.showUsers.setText(
+                getResources().getQuantityString(R.plurals.view_users, users.size(), users.size()));
+        this.binding.usersWrapper.setVisibility(
+                users.size() > 0 || mucOptions.canInvite() ? View.VISIBLE : View.GONE);
         if (users.size() == 0) {
-            this.binding.noUsersHints.setText(mucOptions.isPrivateAndNonAnonymous() ? R.string.no_users_hint_group_chat : R.string.no_users_hint_channel);
+            this.binding.noUsersHints.setText(
+                    mucOptions.isPrivateAndNonAnonymous()
+                            ? R.string.no_users_hint_group_chat
+                            : R.string.no_users_hint_channel);
             this.binding.noUsersHints.setVisibility(View.VISIBLE);
         } else {
             this.binding.noUsersHints.setVisibility(View.GONE);
         }
-
     }
 
     public static String getStatus(Context context, User user, final boolean advanced) {
         if (advanced) {
-            return String.format("%s (%s)", context.getString(user.getAffiliation().getResId()), context.getString(user.getRole().getResId()));
+            return String.format(
+                    "%s (%s)",
+                    context.getString(user.getAffiliation().getResId()),
+                    context.getString(user.getRole().getResId()));
         } else {
             return context.getString(user.getAffiliation().getResId());
         }
@@ -571,7 +675,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         return getStatus(this, user, mAdvancedMode);
     }
 
-
     @Override
     public void onAffiliationChangedSuccessful(Jid jid) {
         refreshUi();
@@ -590,7 +693,11 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
     @Override
     public void onRoomDestroyFailed() {
         final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
-        displayToast(getString(groupChat ? R.string.could_not_destroy_room : R.string.could_not_destroy_channel));
+        displayToast(
+                getString(
+                        groupChat
+                                ? R.string.could_not_destroy_room
+                                : R.string.could_not_destroy_channel));
     }
 
     @Override
@@ -604,23 +711,20 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
     }
 
     private void displayToast(final String msg) {
-        runOnUiThread(() -> {
-            if (isFinishing()) {
-                return;
-            }
-            ToastCompat.makeText(this, msg, Toast.LENGTH_SHORT).show();
-        });
+        runOnUiThread(
+                () -> {
+                    if (isFinishing()) {
+                        return;
+                    }
+                    ToastCompat.makeText(this, msg, Toast.LENGTH_SHORT).show();
+                });
     }
 
     @Override
-    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-
-    }
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
 
     @Override
-    public void onTextChanged(CharSequence s, int start, int before, int count) {
-
-    }
+    public void onTextChanged(CharSequence s, int start, int before, int count) {}
 
     @Override
     public void afterTextChanged(Editable s) {
@@ -629,8 +733,14 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         }
         final MucOptions mucOptions = mConversation.getMucOptions();
         if (this.binding.mucEditor.getVisibility() == View.VISIBLE) {
-            boolean subjectChanged = changed(binding.mucEditSubject.getEditableText().toString(), mucOptions.getSubject());
-            boolean nameChanged = changed(binding.mucEditTitle.getEditableText().toString(), mucOptions.getName());
+            boolean subjectChanged =
+                    changed(
+                            binding.mucEditSubject.getEditableText().toString(),
+                            mucOptions.getSubject());
+            boolean nameChanged =
+                    changed(
+                            binding.mucEditTitle.getEditableText().toString(),
+                            mucOptions.getName());
             if (subjectChanged || nameChanged) {
                 this.binding.editMucNameButton.setImageResource(R.drawable.ic_save_24dp);
                 this.binding.editMucNameButton.setContentDescription(getString(R.string.save));
@@ -640,5 +750,4 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             }
         }
     }
-
 }

src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java 🔗

@@ -26,23 +26,14 @@ import android.widget.CompoundButton;
 import android.widget.CompoundButton.OnCheckedChangeListener;
 import android.widget.TextView;
 import android.widget.Toast;
-
 import androidx.annotation.NonNull;
 import androidx.core.content.ContextCompat;
 import androidx.core.view.ViewCompat;
 import androidx.databinding.DataBindingUtil;
-
 import com.google.android.material.color.MaterialColors;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Ints;
-
-import org.openintents.openpgp.util.OpenPgpUtils;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -78,48 +69,72 @@ import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 import eu.siacs.conversations.xmpp.XmppConnection;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.openintents.openpgp.util.OpenPgpUtils;
 
-public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated, OnMediaLoaded {
+public class ContactDetailsActivity extends OmemoActivity
+        implements OnAccountUpdate,
+                OnRosterUpdate,
+                OnUpdateBlocklist,
+                OnKeyStatusUpdated,
+                OnMediaLoaded {
     public static final String ACTION_VIEW_CONTACT = "view_contact";
     private final int REQUEST_SYNC_CONTACTS = 0x28cf;
     ActivityContactDetailsBinding binding;
     private MediaAdapter mMediaAdapter;
 
     private Contact contact;
-    private final DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() {
+    private final DialogInterface.OnClickListener removeFromRoster =
+            new DialogInterface.OnClickListener() {
 
-        @Override
-        public void onClick(DialogInterface dialog, int which) {
-            xmppConnectionService.deleteContactOnServer(contact);
-        }
-    };
-    private final OnCheckedChangeListener mOnSendCheckedChange = new OnCheckedChangeListener() {
-
-        @Override
-        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
-            if (isChecked) {
-                if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
-                    xmppConnectionService.stopPresenceUpdatesTo(contact);
-                } else {
-                    contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    xmppConnectionService.deleteContactOnServer(contact);
                 }
-            } else {
-                contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
-                xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().stopPresenceUpdatesTo(contact));
-            }
-        }
-    };
-    private final OnCheckedChangeListener mOnReceiveCheckedChange = new OnCheckedChangeListener() {
-
-        @Override
-        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
-            if (isChecked) {
-                xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().requestPresenceUpdatesFrom(contact));
-            } else {
-                xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().stopPresenceUpdatesFrom(contact));
-            }
-        }
-    };
+            };
+    private final OnCheckedChangeListener mOnSendCheckedChange =
+            new OnCheckedChangeListener() {
+
+                @Override
+                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                    if (isChecked) {
+                        if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
+                            xmppConnectionService.stopPresenceUpdatesTo(contact);
+                        } else {
+                            contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
+                        }
+                    } else {
+                        contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+                        xmppConnectionService.sendPresencePacket(
+                                contact.getAccount(),
+                                xmppConnectionService
+                                        .getPresenceGenerator()
+                                        .stopPresenceUpdatesTo(contact));
+                    }
+                }
+            };
+    private final OnCheckedChangeListener mOnReceiveCheckedChange =
+            new OnCheckedChangeListener() {
+
+                @Override
+                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                    if (isChecked) {
+                        xmppConnectionService.sendPresencePacket(
+                                contact.getAccount(),
+                                xmppConnectionService
+                                        .getPresenceGenerator()
+                                        .requestPresenceUpdatesFrom(contact));
+                    } else {
+                        xmppConnectionService.sendPresencePacket(
+                                contact.getAccount(),
+                                xmppConnectionService
+                                        .getPresenceGenerator()
+                                        .stopPresenceUpdatesFrom(contact));
+                    }
+                }
+            };
     private Jid accountJid;
     private Jid contactJid;
     private boolean showDynamicTags = false;
@@ -130,14 +145,18 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
     private void checkContactPermissionAndShowAddDialog() {
         if (hasContactsPermission()) {
             showAddToPhoneBookDialog();
-        } else if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
+        } else if (QuickConversationsService.isContactListIntegration(this)
+                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            requestPermissions(
+                    new String[] {Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
         }
     }
 
     private boolean hasContactsPermission() {
-        if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            return checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
+        if (QuickConversationsService.isContactListIntegration(this)
+                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return checkSelfPermission(Manifest.permission.READ_CONTACTS)
+                    == PackageManager.PERMISSION_GRANTED;
         } else {
             return true;
         }
@@ -145,9 +164,10 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
 
     private void showAddToPhoneBookDialog() {
         final Jid jid = contact.getJid();
-        final boolean quicksyContact = AbstractQuickConversationsService.isQuicksy()
-                && Config.QUICKSY_DOMAIN.equals(jid.getDomain())
-                && jid.getLocal() != null;
+        final boolean quicksyContact =
+                AbstractQuickConversationsService.isQuicksy()
+                        && Config.QUICKSY_DOMAIN.equals(jid.getDomain())
+                        && jid.getLocal() != null;
         final String value;
         if (quicksyContact) {
             value = PhoneNumberUtilWrapper.toFormattedPhoneNumber(this, jid);
@@ -158,24 +178,33 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
         builder.setTitle(getString(R.string.action_add_phone_book));
         builder.setMessage(getString(R.string.add_phone_book_text, value));
         builder.setNegativeButton(getString(R.string.cancel), null);
-        builder.setPositiveButton(getString(R.string.add), (dialog, which) -> {
-            final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
-            intent.setType(Contacts.CONTENT_ITEM_TYPE);
-            if (quicksyContact) {
-                intent.putExtra(Intents.Insert.PHONE, value);
-            } else {
-                intent.putExtra(Intents.Insert.IM_HANDLE, value);
-                intent.putExtra(Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER);
-                //TODO for modern use we want PROTOCOL_CUSTOM and an extra field with a value of 'XMPP'
-                // however we don’t have such a field and thus have to use the legacy PROTOCOL_JABBER
-            }
-            intent.putExtra("finishActivityOnSaveCompleted", true);
-            try {
-                startActivityForResult(intent, 0);
-            } catch (ActivityNotFoundException e) {
-                Toast.makeText(ContactDetailsActivity.this, R.string.no_application_found_to_view_contact, Toast.LENGTH_SHORT).show();
-            }
-        });
+        builder.setPositiveButton(
+                getString(R.string.add),
+                (dialog, which) -> {
+                    final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+                    intent.setType(Contacts.CONTENT_ITEM_TYPE);
+                    if (quicksyContact) {
+                        intent.putExtra(Intents.Insert.PHONE, value);
+                    } else {
+                        intent.putExtra(Intents.Insert.IM_HANDLE, value);
+                        intent.putExtra(
+                                Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER);
+                        // TODO for modern use we want PROTOCOL_CUSTOM and an extra field with a
+                        // value of 'XMPP'
+                        // however we don’t have such a field and thus have to use the legacy
+                        // PROTOCOL_JABBER
+                    }
+                    intent.putExtra("finishActivityOnSaveCompleted", true);
+                    try {
+                        startActivityForResult(intent, 0);
+                    } catch (ActivityNotFoundException e) {
+                        Toast.makeText(
+                                        ContactDetailsActivity.this,
+                                        R.string.no_application_found_to_view_contact,
+                                        Toast.LENGTH_SHORT)
+                                .show();
+                    }
+                });
         builder.create().show();
     }
 
@@ -203,7 +232,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
     @Override
     protected String getShareableUri(boolean http) {
         if (http) {
-            return "https://conversations.im/i/" + XmppUri.lameUrlEncode(contact.getJid().asBareJid().toEscapedString());
+            return "https://conversations.im/i/"
+                    + XmppUri.lameUrlEncode(contact.getJid().asBareJid().toEscapedString());
         } else {
             return "xmpp:" + contact.getJid().asBareJid().toEscapedString();
         }
@@ -212,7 +242,9 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
     @Override
     protected void onCreate(final Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        showInactiveOmemo = savedInstanceState != null && savedInstanceState.getBoolean("show_inactive_omemo", false);
+        showInactiveOmemo =
+                savedInstanceState != null
+                        && savedInstanceState.getBoolean("show_inactive_omemo", false);
         if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) {
             try {
                 this.accountJid = Jid.ofEscaped(getIntent().getExtras().getString(EXTRA_ACCOUNT));
@@ -229,10 +261,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
 
         setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar());
-        binding.showInactiveDevices.setOnClickListener(v -> {
-            showInactiveOmemo = !showInactiveOmemo;
-            populateView();
-        });
+        binding.showInactiveDevices.setOnClickListener(
+                v -> {
+                    showInactiveOmemo = !showInactiveOmemo;
+                    populateView();
+                });
         binding.addContactButton.setOnClickListener(v -> showAddToRosterDialog(contact));
 
         mMediaAdapter = new MediaAdapter(this, R.dimen.media_size);
@@ -252,12 +285,14 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
         final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
         this.showDynamicTags = preferences.getBoolean(AppSettings.SHOW_DYNAMIC_TAGS, false);
         this.showLastSeen = preferences.getBoolean("last_activity", false);
-        binding.mediaWrapper.setVisibility(Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
+        binding.mediaWrapper.setVisibility(
+                Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
         mMediaAdapter.setAttachments(Collections.emptyList());
     }
 
     @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+    public void onRequestPermissionsResult(
+            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
         // TODO check for Camera / Scan permission
         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
         if (grantResults.length > 0)
@@ -289,19 +324,29 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
                 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
                 builder.setNegativeButton(getString(R.string.cancel), null);
                 builder.setTitle(getString(R.string.action_delete_contact))
-                        .setMessage(JidDialog.style(this, R.string.remove_contact_text, contact.getJid().toEscapedString()))
-                        .setPositiveButton(getString(R.string.delete),
-                                removeFromRoster).create().show();
+                        .setMessage(
+                                JidDialog.style(
+                                        this,
+                                        R.string.remove_contact_text,
+                                        contact.getJid().toEscapedString()))
+                        .setPositiveButton(getString(R.string.delete), removeFromRoster)
+                        .create()
+                        .show();
                 break;
             case R.id.action_edit_contact:
-                Uri systemAccount = contact.getSystemAccount();
+                final Uri systemAccount = contact.getSystemAccount();
                 if (systemAccount == null) {
-                    quickEdit(contact.getServerName(), R.string.contact_name, value -> {
-                        contact.setServerName(value);
-                        ContactDetailsActivity.this.xmppConnectionService.pushContactToServer(contact);
-                        populateView();
-                        return null;
-                    }, true);
+                    quickEdit(
+                            contact.getServerName(),
+                            R.string.contact_name,
+                            value -> {
+                                contact.setServerName(value);
+                                ContactDetailsActivity.this.xmppConnectionService
+                                        .pushContactToServer(contact);
+                                populateView();
+                                return null;
+                            },
+                            true);
                 } else {
                     Intent intent = new Intent(Intent.ACTION_EDIT);
                     intent.setDataAndType(systemAccount, Contacts.CONTENT_ITEM_TYPE);
@@ -309,29 +354,42 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
                     try {
                         startActivity(intent);
                     } catch (ActivityNotFoundException e) {
-                        Toast.makeText(ContactDetailsActivity.this, R.string.no_application_found_to_view_contact, Toast.LENGTH_SHORT).show();
+                        Toast.makeText(
+                                        ContactDetailsActivity.this,
+                                        R.string.no_application_found_to_view_contact,
+                                        Toast.LENGTH_SHORT)
+                                .show();
                     }
-
                 }
                 break;
-            case R.id.action_block:
+            case R.id.action_block, R.id.action_unblock:
                 BlockContactDialog.show(this, contact);
                 break;
-            case R.id.action_unblock:
-                BlockContactDialog.show(this, contact);
+            case R.id.action_custom_notifications:
+                configureCustomNotifications(contact);
                 break;
         }
         return super.onOptionsItemSelected(menuItem);
     }
 
+    private void configureCustomNotifications(final Contact contact) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            return;
+        }
+        final var shortcut = xmppConnectionService.getShortcutService().getShortcutInfo(contact);
+        configureCustomNotification(shortcut);
+    }
+
     @Override
     public boolean onCreateOptionsMenu(final Menu menu) {
         getMenuInflater().inflate(R.menu.contact_details, menu);
         AccountUtils.showHideMenuItems(menu);
-        MenuItem block = menu.findItem(R.id.action_block);
-        MenuItem unblock = menu.findItem(R.id.action_unblock);
-        MenuItem edit = menu.findItem(R.id.action_edit_contact);
-        MenuItem delete = menu.findItem(R.id.action_delete_contact);
+        final MenuItem block = menu.findItem(R.id.action_block);
+        final MenuItem unblock = menu.findItem(R.id.action_unblock);
+        final MenuItem edit = menu.findItem(R.id.action_edit_contact);
+        final MenuItem delete = menu.findItem(R.id.action_delete_contact);
+        final MenuItem customNotifications = menu.findItem(R.id.action_custom_notifications);
+        customNotifications.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R);
         if (contact == null) {
             return true;
         }
@@ -374,7 +432,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
                 binding.statusMessage.setVisibility(View.VISIBLE);
                 final Spannable span = new SpannableString(message);
                 if (Emoticons.isOnlyEmoji(message)) {
-                    span.setSpan(new RelativeSizeSpan(2.0f), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                    span.setSpan(
+                            new RelativeSizeSpan(2.0f),
+                            0,
+                            message.length(),
+                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                 }
                 binding.statusMessage.setText(span);
             } else {
@@ -398,14 +460,16 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
                 binding.detailsSendPresence.setText(R.string.send_presence_updates);
             } else {
                 binding.detailsSendPresence.setText(R.string.preemptively_grant);
-                binding.detailsSendPresence.setChecked(contact.getOption(Contact.Options.PREEMPTIVE_GRANT));
+                binding.detailsSendPresence.setChecked(
+                        contact.getOption(Contact.Options.PREEMPTIVE_GRANT));
             }
             if (contact.getOption(Contact.Options.TO)) {
                 binding.detailsReceivePresence.setText(R.string.receive_presence_updates);
                 binding.detailsReceivePresence.setChecked(true);
             } else {
                 binding.detailsReceivePresence.setText(R.string.ask_for_presence_updates);
-                binding.detailsReceivePresence.setChecked(contact.getOption(Contact.Options.ASKING));
+                binding.detailsReceivePresence.setChecked(
+                        contact.getOption(Contact.Options.ASKING));
             }
             if (contact.getAccount().isOnlineAndConnected()) {
                 binding.detailsReceivePresence.setEnabled(true);
@@ -431,7 +495,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
                     && contact.getLastseen() > 0
                     && contact.getPresences().allOrNonSupport(Namespace.IDLE)) {
                 binding.detailsLastseen.setVisibility(View.VISIBLE);
-                binding.detailsLastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen()));
+                binding.detailsLastseen.setText(
+                        UIHelper.lastseen(
+                                getApplicationContext(),
+                                contact.isActive(),
+                                contact.getLastseen()));
             } else {
                 binding.detailsLastseen.setVisibility(View.GONE);
             }
@@ -440,7 +508,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
         binding.detailsContactjid.setText(IrregularUnicodeDetector.style(this, contact.getJid()));
         final String account = contact.getAccount().getJid().asBareJid().toEscapedString();
         binding.detailsAccount.setText(getString(R.string.using_account, account));
-        AvatarWorkerTask.loadAvatar(contact, binding.detailsContactBadge, R.dimen.avatar_on_details_screen_size);
+        AvatarWorkerTask.loadAvatar(
+                contact, binding.detailsContactBadge, R.dimen.avatar_on_details_screen_size);
         binding.detailsContactBadge.setOnClickListener(this::onBadgeClick);
 
         binding.detailsContactKeys.removeAllViews();
@@ -448,7 +517,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
         final LayoutInflater inflater = getLayoutInflater();
         final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
         if (Config.supportOmemo() && axolotlService != null) {
-            final Collection<XmppAxolotlSession> sessions = axolotlService.findSessionsForContact(contact);
+            final Collection<XmppAxolotlSession> sessions =
+                    axolotlService.findSessionsForContact(contact);
             boolean anyActive = false;
             for (XmppAxolotlSession session : sessions) {
                 anyActive = session.getTrust().isActive();
@@ -478,9 +548,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
                     showUnverifiedWarning = true;
                 }
             }
-            binding.unverifiedWarning.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE);
+            binding.unverifiedWarning.setVisibility(
+                    showUnverifiedWarning ? View.VISIBLE : View.GONE);
             if (showsInactive || skippedInactive) {
-                binding.showInactiveDevices.setText(showsInactive ? R.string.hide_inactive_devices : R.string.show_inactive_devices);
+                binding.showInactiveDevices.setText(
+                        showsInactive
+                                ? R.string.hide_inactive_devices
+                                : R.string.show_inactive_devices);
                 binding.showInactiveDevices.setVisibility(View.VISIBLE);
             } else {
                 binding.showInactiveDevices.setVisibility(View.GONE);
@@ -489,7 +563,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
             binding.showInactiveDevices.setVisibility(View.GONE);
         }
         final boolean isCameraFeatureAvailable = isCameraFeatureAvailable();
-        binding.scanButton.setVisibility(hasKeys && isCameraFeatureAvailable ? View.VISIBLE : View.GONE);
+        binding.scanButton.setVisibility(
+                hasKeys && isCameraFeatureAvailable ? View.VISIBLE : View.GONE);
         if (hasKeys) {
             binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this));
         }
@@ -500,7 +575,9 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
             TextView keyType = view.findViewById(R.id.key_type);
             keyType.setText(R.string.openpgp_key_id);
             if ("pgp".equals(messageFingerprint)) {
-                keyType.setTextColor(MaterialColors.getColor(keyType, com.google.android.material.R.attr.colorPrimaryVariant));
+                keyType.setTextColor(
+                        MaterialColors.getColor(
+                                keyType, com.google.android.material.R.attr.colorPrimaryVariant));
             }
             key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId()));
             final OnClickListener openKey = v -> launchOpenKeyChain(contact.getPgpKeyId());
@@ -512,7 +589,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
         binding.keysWrapper.setVisibility(hasKeys ? View.VISIBLE : View.GONE);
 
         final List<ListItem.Tag> tagList = contact.getTags(this);
-        final boolean hasMetaTags = contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE;
+        final boolean hasMetaTags =
+                contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE;
         if ((tagList.isEmpty() && !hasMetaTags) || !this.showDynamicTags) {
             binding.tags.setVisibility(View.GONE);
         } else {
@@ -521,9 +599,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
             final ImmutableList.Builder<Integer> viewIdBuilder = new ImmutableList.Builder<>();
             for (final ListItem.Tag tag : tagList) {
                 final String name = tag.getName();
-                final TextView tv = (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false);
+                final TextView tv =
+                        (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false);
                 tv.setText(name);
-                tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(this,XEP0392Helper.rgbFromNick(name))));
+                tv.setBackgroundTintList(
+                        ColorStateList.valueOf(
+                                MaterialColors.harmonizeWithPrimary(
+                                        this, XEP0392Helper.rgbFromNick(name))));
                 final int id = ViewCompat.generateViewId();
                 tv.setId(id);
                 viewIdBuilder.add(id);
@@ -531,11 +613,14 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
             }
             if (contact.isBlocked()) {
                 final TextView tv =
-                        (TextView)
-                                inflater.inflate(
-                                        R.layout.item_tag, binding.tags, false);
+                        (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false);
                 tv.setText(R.string.blocked);
-                tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(tv.getContext(), ContextCompat.getColor(tv.getContext(),R.color.gray_800))));
+                tv.setBackgroundTintList(
+                        ColorStateList.valueOf(
+                                MaterialColors.harmonizeWithPrimary(
+                                        tv.getContext(),
+                                        ContextCompat.getColor(
+                                                tv.getContext(), R.color.gray_800))));
                 final int id = ViewCompat.generateViewId();
                 tv.setId(id);
                 viewIdBuilder.add(id);
@@ -544,9 +629,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
                 final Presence.Status status = contact.getShownStatus();
                 if (status != Presence.Status.OFFLINE) {
                     final TextView tv =
-                            (TextView)
-                                    inflater.inflate(
-                                            R.layout.item_tag, binding.tags, false);
+                            (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false);
                     UIHelper.setStatus(tv, status);
                     final int id = ViewCompat.generateViewId();
                     tv.setId(id);
@@ -599,8 +682,10 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
 
             if (Compatibility.hasStoragePermission(this)) {
                 final int limit = GridManager.getCurrentColumnCount(this.binding.media);
-                xmppConnectionService.getAttachments(account, contact.getJid().asBareJid(), limit, this);
-                this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, contact));
+                xmppConnectionService.getAttachments(
+                        account, contact.getJid().asBareJid(), limit, this);
+                this.binding.showMedia.setOnClickListener(
+                        (v) -> MediaBrowserActivity.launch(this, contact));
             }
             populateView();
         }
@@ -613,7 +698,9 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
 
     @Override
     protected void processFingerprintVerification(XmppUri uri) {
-        if (contact != null && contact.getJid().asBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
+        if (contact != null
+                && contact.getJid().asBareJid().equals(uri.getJid())
+                && uri.hasFingerprints()) {
             if (xmppConnectionService.verifyFingerprints(contact, uri.getFingerprints())) {
                 Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
             }
@@ -624,11 +711,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
 
     @Override
     public void onMediaLoaded(List<Attachment> attachments) {
-        runOnUiThread(() -> {
-            int limit = GridManager.getCurrentColumnCount(binding.media);
-            mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit, attachments.size())));
-            binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE);
-        });
-
+        runOnUiThread(
+                () -> {
+                    int limit = GridManager.getCurrentColumnCount(binding.media);
+                    mMediaAdapter.setAttachments(
+                            attachments.subList(0, Math.min(limit, attachments.size())));
+                    binding.mediaWrapper.setVisibility(
+                            attachments.size() > 0 ? View.VISIBLE : View.GONE);
+                });
     }
 }

src/main/java/eu/siacs/conversations/ui/XmppActivity.java 🔗

@@ -2,6 +2,7 @@ package eu.siacs.conversations.ui;
 
 import android.Manifest;
 import android.annotation.SuppressLint;
+import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.ActivityNotFoundException;
 import android.content.ClipData;
@@ -15,7 +16,6 @@ import android.content.IntentSender.SendIntentException;
 import android.content.ServiceConnection;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Point;
@@ -31,6 +31,7 @@ import android.os.IBinder;
 import android.os.PowerManager;
 import android.os.SystemClock;
 import android.preference.PreferenceManager;
+import android.provider.Settings;
 import android.text.Html;
 import android.text.InputType;
 import android.util.DisplayMetrics;
@@ -42,19 +43,19 @@ import android.widget.Button;
 import android.widget.CheckBox;
 import android.widget.ImageView;
 import android.widget.Toast;
-
 import androidx.annotation.BoolRes;
 import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
 import androidx.annotation.StringRes;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AppCompatDelegate;
+import androidx.core.content.pm.ShortcutInfoCompat;
+import androidx.core.content.pm.ShortcutManagerCompat;
 import androidx.databinding.DataBindingUtil;
-
 import com.google.android.material.color.MaterialColors;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
-
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.BuildConfig;
 import eu.siacs.conversations.Config;
@@ -70,6 +71,7 @@ import eu.siacs.conversations.entities.Presences;
 import eu.siacs.conversations.entities.Reaction;
 import eu.siacs.conversations.services.AvatarService;
 import eu.siacs.conversations.services.BarcodeProvider;
+import eu.siacs.conversations.services.NotificationService;
 import eu.siacs.conversations.services.QuickConversationsService;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
@@ -79,16 +81,13 @@ import eu.siacs.conversations.ui.util.SettingsUtils;
 import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
 import eu.siacs.conversations.utils.AccountUtils;
 import eu.siacs.conversations.utils.Compatibility;
-import eu.siacs.conversations.utils.ExceptionHelper;
 import eu.siacs.conversations.utils.SignupUtils;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
-
 import java.io.IOException;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.RejectedExecutionException;
@@ -112,50 +111,58 @@ public abstract class XmppActivity extends ActionBarActivity {
     protected boolean mUsingEnterKey = false;
     protected boolean mUseTor = false;
     protected Toast mToast;
-    public Runnable onOpenPGPKeyPublished = () -> Toast.makeText(XmppActivity.this, R.string.openpgp_has_been_published, Toast.LENGTH_SHORT).show();
+    public Runnable onOpenPGPKeyPublished =
+            () ->
+                    Toast.makeText(
+                                    XmppActivity.this,
+                                    R.string.openpgp_has_been_published,
+                                    Toast.LENGTH_SHORT)
+                            .show();
     protected ConferenceInvite mPendingConferenceInvite = null;
-    protected ServiceConnection mConnection = new ServiceConnection() {
+    protected ServiceConnection mConnection =
+            new ServiceConnection() {
 
-        @Override
-        public void onServiceConnected(ComponentName className, IBinder service) {
-            XmppConnectionBinder binder = (XmppConnectionBinder) service;
-            xmppConnectionService = binder.getService();
-            xmppConnectionServiceBound = true;
-            registerListeners();
-            onBackendConnected();
-        }
+                @Override
+                public void onServiceConnected(ComponentName className, IBinder service) {
+                    XmppConnectionBinder binder = (XmppConnectionBinder) service;
+                    xmppConnectionService = binder.getService();
+                    xmppConnectionServiceBound = true;
+                    registerListeners();
+                    onBackendConnected();
+                }
 
-        @Override
-        public void onServiceDisconnected(ComponentName arg0) {
-            xmppConnectionServiceBound = false;
-        }
-    };
+                @Override
+                public void onServiceDisconnected(ComponentName arg0) {
+                    xmppConnectionServiceBound = false;
+                }
+            };
     private DisplayMetrics metrics;
     private long mLastUiRefresh = 0;
     private final Handler mRefreshUiHandler = new Handler();
-    private final Runnable mRefreshUiRunnable = () -> {
-        mLastUiRefresh = SystemClock.elapsedRealtime();
-        refreshUiReal();
-    };
-    private final UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
-        @Override
-        public void success(final Conversation conversation) {
-            runOnUiThread(() -> {
-                switchToConversation(conversation);
-                hideToast();
-            });
-        }
-
-        @Override
-        public void error(final int errorCode, Conversation object) {
-            runOnUiThread(() -> replaceToast(getString(errorCode)));
-        }
+    private final Runnable mRefreshUiRunnable =
+            () -> {
+                mLastUiRefresh = SystemClock.elapsedRealtime();
+                refreshUiReal();
+            };
+    private final UiCallback<Conversation> adhocCallback =
+            new UiCallback<Conversation>() {
+                @Override
+                public void success(final Conversation conversation) {
+                    runOnUiThread(
+                            () -> {
+                                switchToConversation(conversation);
+                                hideToast();
+                            });
+                }
 
-        @Override
-        public void userInputRequired(PendingIntent pi, Conversation object) {
+                @Override
+                public void error(final int errorCode, Conversation object) {
+                    runOnUiThread(() -> replaceToast(getString(errorCode)));
+                }
 
-        }
-    };
+                @Override
+                public void userInputRequired(PendingIntent pi, Conversation object) {}
+            };
 
     public static boolean cancelPotentialWork(Message message, ImageView imageView) {
         final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
@@ -212,7 +219,7 @@ public abstract class XmppActivity extends ActionBarActivity {
         }
     }
 
-    abstract protected void refreshUiReal();
+    protected abstract void refreshUiReal();
 
     @Override
     public void onStart() {
@@ -248,6 +255,41 @@ public abstract class XmppActivity extends ActionBarActivity {
         }
     }
 
+    @RequiresApi(api = Build.VERSION_CODES.R)
+    protected void configureCustomNotification(final ShortcutInfoCompat shortcut) {
+        final var notificationManager = getSystemService(NotificationManager.class);
+        final var channel =
+                notificationManager.getNotificationChannel(
+                        NotificationService.MESSAGES_NOTIFICATION_CHANNEL, shortcut.getId());
+        if (channel != null && channel.getConversationId() != null) {
+            ShortcutManagerCompat.pushDynamicShortcut(this, shortcut);
+            openNotificationSettings(shortcut);
+        } else {
+            new MaterialAlertDialogBuilder(this)
+                    .setTitle(R.string.custom_notifications)
+                    .setMessage(R.string.custom_notifications_enable)
+                    .setPositiveButton(
+                            R.string.continue_btn,
+                            (d, w) -> {
+                                NotificationService.createConversationChannel(this, shortcut);
+                                ShortcutManagerCompat.pushDynamicShortcut(this, shortcut);
+                                openNotificationSettings(shortcut);
+                            })
+                    .setNegativeButton(R.string.cancel, null)
+                    .create()
+                    .show();
+        }
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.R)
+    protected void openNotificationSettings(final ShortcutInfoCompat shortcut) {
+        final var intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
+        intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
+        intent.putExtra(
+                Settings.EXTRA_CHANNEL_ID, NotificationService.MESSAGES_NOTIFICATION_CHANNEL);
+        intent.putExtra(Settings.EXTRA_CONVERSATION_ID, shortcut.getId());
+        startActivity(intent);
+    }
 
     public boolean hasPgp() {
         return xmppConnectionService.getPgpEngine() != null;
@@ -257,16 +299,20 @@ public abstract class XmppActivity extends ActionBarActivity {
         final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setTitle(getString(R.string.openkeychain_required));
         builder.setIconAttribute(android.R.attr.alertDialogIcon);
-        builder.setMessage(Html.fromHtml(getString(R.string.openkeychain_required_long, getString(R.string.app_name))));
+        builder.setMessage(
+                Html.fromHtml(
+                        getString(
+                                R.string.openkeychain_required_long,
+                                getString(R.string.app_name))));
         builder.setNegativeButton(getString(R.string.cancel), null);
-        builder.setNeutralButton(getString(R.string.restart),
+        builder.setNeutralButton(
+                getString(R.string.restart),
                 (dialog, which) -> {
                     if (xmppConnectionServiceBound) {
                         unbindService(mConnection);
                         xmppConnectionServiceBound = false;
                     }
-                    stopService(new Intent(XmppActivity.this,
-                            XmppConnectionService.class));
+                    stopService(new Intent(XmppActivity.this, XmppConnectionService.class));
                     finish();
                 });
         builder.setPositiveButton(
@@ -344,58 +390,89 @@ public abstract class XmppActivity extends ActionBarActivity {
     protected void deleteAccount(final Account account, final Runnable postDelete) {
         final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         final View dialogView = getLayoutInflater().inflate(R.layout.dialog_delete_account, null);
-        final CheckBox deleteFromServer =
-                dialogView.findViewById(R.id.delete_from_server);
+        final CheckBox deleteFromServer = dialogView.findViewById(R.id.delete_from_server);
         builder.setView(dialogView);
         builder.setTitle(R.string.mgmt_account_delete);
-        builder.setPositiveButton(getString(R.string.delete),null);
+        builder.setPositiveButton(getString(R.string.delete), null);
         builder.setNegativeButton(getString(R.string.cancel), null);
         final AlertDialog dialog = builder.create();
-        dialog.setOnShowListener(dialogInterface->{
-            final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
-            button.setOnClickListener(v -> {
-                final boolean unregister = deleteFromServer.isChecked();
-                if (unregister) {
-                    if (account.isOnlineAndConnected()) {
-                        deleteFromServer.setEnabled(false);
-                        button.setText(R.string.please_wait);
-                        button.setEnabled(false);
-                        xmppConnectionService.unregisterAccount(account, result -> {
-                            runOnUiThread(()->{
-                                if (result) {
-                                    dialog.dismiss();
-                                    if (postDelete != null) {
-                                        postDelete.run();
+        dialog.setOnShowListener(
+                dialogInterface -> {
+                    final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+                    button.setOnClickListener(
+                            v -> {
+                                final boolean unregister = deleteFromServer.isChecked();
+                                if (unregister) {
+                                    if (account.isOnlineAndConnected()) {
+                                        deleteFromServer.setEnabled(false);
+                                        button.setText(R.string.please_wait);
+                                        button.setEnabled(false);
+                                        xmppConnectionService.unregisterAccount(
+                                                account,
+                                                result -> {
+                                                    runOnUiThread(
+                                                            () -> {
+                                                                if (result) {
+                                                                    dialog.dismiss();
+                                                                    if (postDelete != null) {
+                                                                        postDelete.run();
+                                                                    }
+                                                                    if (xmppConnectionService
+                                                                                            .getAccounts()
+                                                                                            .size()
+                                                                                    == 0
+                                                                            && Config
+                                                                                            .MAGIC_CREATE_DOMAIN
+                                                                                    != null) {
+                                                                        final Intent intent =
+                                                                                SignupUtils
+                                                                                        .getSignUpIntent(
+                                                                                                this);
+                                                                        intent.setFlags(
+                                                                                Intent
+                                                                                                .FLAG_ACTIVITY_NEW_TASK
+                                                                                        | Intent
+                                                                                                .FLAG_ACTIVITY_CLEAR_TASK);
+                                                                        startActivity(intent);
+                                                                    }
+                                                                } else {
+                                                                    deleteFromServer.setEnabled(
+                                                                            true);
+                                                                    button.setText(R.string.delete);
+                                                                    button.setEnabled(true);
+                                                                    Toast.makeText(
+                                                                                    this,
+                                                                                    R.string
+                                                                                            .could_not_delete_account_from_server,
+                                                                                    Toast
+                                                                                            .LENGTH_LONG)
+                                                                            .show();
+                                                                }
+                                                            });
+                                                });
+                                    } else {
+                                        Toast.makeText(
+                                                        this,
+                                                        R.string.not_connected_try_again,
+                                                        Toast.LENGTH_LONG)
+                                                .show();
                                     }
-                                    if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) {
+                                } else {
+                                    xmppConnectionService.deleteAccount(account);
+                                    dialog.dismiss();
+                                    if (xmppConnectionService.getAccounts().size() == 0
+                                            && Config.MAGIC_CREATE_DOMAIN != null) {
                                         final Intent intent = SignupUtils.getSignUpIntent(this);
-                                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+                                        intent.setFlags(
+                                                Intent.FLAG_ACTIVITY_NEW_TASK
+                                                        | Intent.FLAG_ACTIVITY_CLEAR_TASK);
                                         startActivity(intent);
+                                    } else if (postDelete != null) {
+                                        postDelete.run();
                                     }
-                                } else {
-                                    deleteFromServer.setEnabled(true);
-                                    button.setText(R.string.delete);
-                                    button.setEnabled(true);
-                                    Toast.makeText(this,R.string.could_not_delete_account_from_server,Toast.LENGTH_LONG).show();
                                 }
                             });
-                        });
-                    } else {
-                        Toast.makeText(this,R.string.not_connected_try_again,Toast.LENGTH_LONG).show();
-                    }
-                } else {
-                    xmppConnectionService.deleteAccount(account);
-                    dialog.dismiss();
-                    if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) {
-                        final Intent intent = SignupUtils.getSignUpIntent(this);
-                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-                        startActivity(intent);
-                    } else if (postDelete != null) {
-                        postDelete.run();
-                    }
-                }
-            });
-        });
+                });
         dialog.show();
     }
 
@@ -403,61 +480,75 @@ public abstract class XmppActivity extends ActionBarActivity {
 
     protected void registerListeners() {
         if (this instanceof XmppConnectionService.OnConversationUpdate) {
-            this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this);
+            this.xmppConnectionService.setOnConversationListChangedListener(
+                    (XmppConnectionService.OnConversationUpdate) this);
         }
         if (this instanceof XmppConnectionService.OnAccountUpdate) {
-            this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
+            this.xmppConnectionService.setOnAccountListChangedListener(
+                    (XmppConnectionService.OnAccountUpdate) this);
         }
         if (this instanceof XmppConnectionService.OnCaptchaRequested) {
-            this.xmppConnectionService.setOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this);
+            this.xmppConnectionService.setOnCaptchaRequestedListener(
+                    (XmppConnectionService.OnCaptchaRequested) this);
         }
         if (this instanceof XmppConnectionService.OnRosterUpdate) {
-            this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
+            this.xmppConnectionService.setOnRosterUpdateListener(
+                    (XmppConnectionService.OnRosterUpdate) this);
         }
         if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
-            this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this);
+            this.xmppConnectionService.setOnMucRosterUpdateListener(
+                    (XmppConnectionService.OnMucRosterUpdate) this);
         }
         if (this instanceof OnUpdateBlocklist) {
             this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this);
         }
         if (this instanceof XmppConnectionService.OnShowErrorToast) {
-            this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
+            this.xmppConnectionService.setOnShowErrorToastListener(
+                    (XmppConnectionService.OnShowErrorToast) this);
         }
         if (this instanceof OnKeyStatusUpdated) {
             this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this);
         }
         if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) {
-            this.xmppConnectionService.setOnRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this);
+            this.xmppConnectionService.setOnRtpConnectionUpdateListener(
+                    (XmppConnectionService.OnJingleRtpConnectionUpdate) this);
         }
     }
 
     protected void unregisterListeners() {
         if (this instanceof XmppConnectionService.OnConversationUpdate) {
-            this.xmppConnectionService.removeOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this);
+            this.xmppConnectionService.removeOnConversationListChangedListener(
+                    (XmppConnectionService.OnConversationUpdate) this);
         }
         if (this instanceof XmppConnectionService.OnAccountUpdate) {
-            this.xmppConnectionService.removeOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
+            this.xmppConnectionService.removeOnAccountListChangedListener(
+                    (XmppConnectionService.OnAccountUpdate) this);
         }
         if (this instanceof XmppConnectionService.OnCaptchaRequested) {
-            this.xmppConnectionService.removeOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this);
+            this.xmppConnectionService.removeOnCaptchaRequestedListener(
+                    (XmppConnectionService.OnCaptchaRequested) this);
         }
         if (this instanceof XmppConnectionService.OnRosterUpdate) {
-            this.xmppConnectionService.removeOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
+            this.xmppConnectionService.removeOnRosterUpdateListener(
+                    (XmppConnectionService.OnRosterUpdate) this);
         }
         if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
-            this.xmppConnectionService.removeOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this);
+            this.xmppConnectionService.removeOnMucRosterUpdateListener(
+                    (XmppConnectionService.OnMucRosterUpdate) this);
         }
         if (this instanceof OnUpdateBlocklist) {
             this.xmppConnectionService.removeOnUpdateBlocklistListener((OnUpdateBlocklist) this);
         }
         if (this instanceof XmppConnectionService.OnShowErrorToast) {
-            this.xmppConnectionService.removeOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
+            this.xmppConnectionService.removeOnShowErrorToastListener(
+                    (XmppConnectionService.OnShowErrorToast) this);
         }
         if (this instanceof OnKeyStatusUpdated) {
             this.xmppConnectionService.removeOnNewKeysAvailableListener((OnKeyStatusUpdated) this);
         }
         if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) {
-            this.xmppConnectionService.removeRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this);
+            this.xmppConnectionService.removeRtpConnectionUpdateListener(
+                    (XmppConnectionService.OnJingleRtpConnectionUpdate) this);
         }
     }
 
@@ -465,7 +556,9 @@ public abstract class XmppActivity extends ActionBarActivity {
     public boolean onOptionsItemSelected(final MenuItem item) {
         switch (item.getItemId()) {
             case R.id.action_settings:
-                startActivity(new Intent(this, eu.siacs.conversations.ui.activity.SettingsActivity.class));
+                startActivity(
+                        new Intent(
+                                this, eu.siacs.conversations.ui.activity.SettingsActivity.class));
                 break;
             case R.id.action_privacy_policy:
                 openPrivacyPolicy();
@@ -500,7 +593,8 @@ public abstract class XmppActivity extends ActionBarActivity {
         }
     }
 
-    public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
+    public void selectPresence(
+            final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
         final Contact contact = conversation.getContact();
         if (contact.showInRoster() || contact.isSelf()) {
             final Presences presences = contact.getPresences();
@@ -521,7 +615,8 @@ public abstract class XmppActivity extends ActionBarActivity {
                 }
             } else if (presences.size() == 1) {
                 final String presence = presences.toResourceArray()[0];
-                conversation.setNextCounterpart(PresenceSelector.getNextCounterpart(contact, presence));
+                conversation.setNextCounterpart(
+                        PresenceSelector.getNextCounterpart(contact, presence));
                 listener.onPresenceSelected();
             } else {
                 PresenceSelector.showPresenceSelectionDialog(this, conversation, listener);
@@ -536,7 +631,8 @@ public abstract class XmppActivity extends ActionBarActivity {
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         metrics = getResources().getDisplayMetrics();
-        this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
+        this.isCameraFeatureAvailable =
+                getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
     }
 
     protected boolean isCameraFeatureAvailable() {
@@ -546,14 +642,16 @@ public abstract class XmppActivity extends ActionBarActivity {
     protected boolean isOptimizingBattery() {
         final PowerManager pm = getSystemService(PowerManager.class);
         return !pm.isIgnoringBatteryOptimizations(getPackageName());
-}
+    }
 
     protected boolean isAffectedByDataSaver() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-            final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
+            final ConnectivityManager cm =
+                    (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
             return cm != null
                     && cm.isActiveNetworkMetered()
-                    && Compatibility.getRestrictBackgroundStatus(cm) == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
+                    && Compatibility.getRestrictBackgroundStatus(cm)
+                            == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
         } else {
             return false;
         }
@@ -564,7 +662,8 @@ public abstract class XmppActivity extends ActionBarActivity {
     }
 
     private boolean useTor() {
-        return QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor);
+        return QuickConversationsService.isConversations()
+                && getBooleanPreference("use_tor", R.bool.use_tor);
     }
 
     protected SharedPreferences getPreferences() {
@@ -599,7 +698,13 @@ public abstract class XmppActivity extends ActionBarActivity {
         switchToConversation(conversation, null, false, nick, true, false);
     }
 
-    private void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend) {
+    private void switchToConversation(
+            Conversation conversation,
+            String text,
+            boolean asQuote,
+            String nick,
+            boolean pm,
+            boolean doNotAppend) {
         Intent intent = new Intent(this, ConversationsActivity.class);
         intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
         intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
@@ -647,7 +752,10 @@ public abstract class XmppActivity extends ActionBarActivity {
         intent.putExtra("jid", account.getJid().asBareJid().toEscapedString());
         intent.putExtra("init", init);
         if (init) {
-            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION);
+            intent.setFlags(
+                    Intent.FLAG_ACTIVITY_NEW_TASK
+                            | Intent.FLAG_ACTIVITY_CLEAR_TASK
+                            | Intent.FLAG_ACTIVITY_NO_ANIMATION);
         }
         if (fingerprint != null) {
             intent.putExtra("fingerprint", fingerprint);
@@ -671,85 +779,113 @@ public abstract class XmppActivity extends ActionBarActivity {
     }
 
     protected void inviteToConversation(Conversation conversation) {
-        startActivityForResult(ChooseContactActivity.create(this, conversation), REQUEST_INVITE_TO_CONVERSATION);
+        startActivityForResult(
+                ChooseContactActivity.create(this, conversation), REQUEST_INVITE_TO_CONVERSATION);
     }
 
-    protected void announcePgp(final Account account, final Conversation conversation, Intent intent, final Runnable onSuccess) {
+    protected void announcePgp(
+            final Account account,
+            final Conversation conversation,
+            Intent intent,
+            final Runnable onSuccess) {
         if (account.getPgpId() == 0) {
             choosePgpSignId(account);
         } else {
             final String status = Strings.nullToEmpty(account.getPresenceStatusMessage());
-            xmppConnectionService.getPgpEngine().generateSignature(intent, account, status, new UiCallback<String>() {
-
-                @Override
-                public void userInputRequired(final PendingIntent pi, final String signature) {
-                    try {
-                        startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0,Compatibility.pgpStartIntentSenderOptions());
-                    } catch (final SendIntentException ignored) {
-                    }
-                }
+            xmppConnectionService
+                    .getPgpEngine()
+                    .generateSignature(
+                            intent,
+                            account,
+                            status,
+                            new UiCallback<String>() {
+
+                                @Override
+                                public void userInputRequired(
+                                        final PendingIntent pi, final String signature) {
+                                    try {
+                                        startIntentSenderForResult(
+                                                pi.getIntentSender(),
+                                                REQUEST_ANNOUNCE_PGP,
+                                                null,
+                                                0,
+                                                0,
+                                                0,
+                                                Compatibility.pgpStartIntentSenderOptions());
+                                    } catch (final SendIntentException ignored) {
+                                    }
+                                }
 
-                @Override
-                public void success(String signature) {
-                    account.setPgpSignature(signature);
-                    xmppConnectionService.databaseBackend.updateAccount(account);
-                    xmppConnectionService.sendPresence(account);
-                    if (conversation != null) {
-                        conversation.setNextEncryption(Message.ENCRYPTION_PGP);
-                        xmppConnectionService.updateConversation(conversation);
-                        refreshUi();
-                    }
-                    if (onSuccess != null) {
-                        runOnUiThread(onSuccess);
-                    }
-                }
+                                @Override
+                                public void success(String signature) {
+                                    account.setPgpSignature(signature);
+                                    xmppConnectionService.databaseBackend.updateAccount(account);
+                                    xmppConnectionService.sendPresence(account);
+                                    if (conversation != null) {
+                                        conversation.setNextEncryption(Message.ENCRYPTION_PGP);
+                                        xmppConnectionService.updateConversation(conversation);
+                                        refreshUi();
+                                    }
+                                    if (onSuccess != null) {
+                                        runOnUiThread(onSuccess);
+                                    }
+                                }
 
-                @Override
-                public void error(int error, String signature) {
-                    if (error == 0) {
-                        account.setPgpSignId(0);
-                        account.unsetPgpSignature();
-                        xmppConnectionService.databaseBackend.updateAccount(account);
-                        choosePgpSignId(account);
-                    } else {
-                        displayErrorDialog(error);
-                    }
-                }
-            });
+                                @Override
+                                public void error(int error, String signature) {
+                                    if (error == 0) {
+                                        account.setPgpSignId(0);
+                                        account.unsetPgpSignature();
+                                        xmppConnectionService.databaseBackend.updateAccount(
+                                                account);
+                                        choosePgpSignId(account);
+                                    } else {
+                                        displayErrorDialog(error);
+                                    }
+                                }
+                            });
         }
     }
 
     protected void choosePgpSignId(final Account account) {
-        xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<>() {
-            @Override
-            public void success(final Account a) {
-            }
-
-            @Override
-            public void error(int errorCode, Account object) {
-
-            }
-
-            @Override
-            public void userInputRequired(PendingIntent pi, Account object) {
-                try {
-                    startIntentSenderForResult(pi.getIntentSender(),
-                            REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions());
-                } catch (final SendIntentException ignored) {
-                }
-            }
-        });
+        xmppConnectionService
+                .getPgpEngine()
+                .chooseKey(
+                        account,
+                        new UiCallback<>() {
+                            @Override
+                            public void success(final Account a) {}
+
+                            @Override
+                            public void error(int errorCode, Account object) {}
+
+                            @Override
+                            public void userInputRequired(PendingIntent pi, Account object) {
+                                try {
+                                    startIntentSenderForResult(
+                                            pi.getIntentSender(),
+                                            REQUEST_CHOOSE_PGP_ID,
+                                            null,
+                                            0,
+                                            0,
+                                            0,
+                                            Compatibility.pgpStartIntentSenderOptions());
+                                } catch (final SendIntentException ignored) {
+                                }
+                            }
+                        });
     }
 
     protected void displayErrorDialog(final int errorCode) {
-        runOnUiThread(() -> {
-            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(XmppActivity.this);
-            builder.setTitle(getString(R.string.error));
-            builder.setMessage(errorCode);
-            builder.setNeutralButton(R.string.accept, null);
-            builder.create().show();
-        });
-
+        runOnUiThread(
+                () -> {
+                    final MaterialAlertDialogBuilder builder =
+                            new MaterialAlertDialogBuilder(XmppActivity.this);
+                    builder.setTitle(getString(R.string.error));
+                    builder.setMessage(errorCode);
+                    builder.setNeutralButton(R.string.accept, null);
+                    builder.create().show();
+                });
     }
 
     protected void showAddToRosterDialog(final Contact contact) {
@@ -757,7 +893,9 @@ public abstract class XmppActivity extends ActionBarActivity {
         builder.setTitle(contact.getJid().toString());
         builder.setMessage(getString(R.string.not_in_roster));
         builder.setNegativeButton(getString(R.string.cancel), null);
-        builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> xmppConnectionService.createContact(contact, true));
+        builder.setPositiveButton(
+                getString(R.string.add_contact),
+                (dialog, which) -> xmppConnectionService.createContact(contact, true));
         builder.create().show();
     }
 
@@ -766,13 +904,15 @@ public abstract class XmppActivity extends ActionBarActivity {
         builder.setTitle(contact.getJid().toString());
         builder.setMessage(R.string.request_presence_updates);
         builder.setNegativeButton(R.string.cancel, null);
-        builder.setPositiveButton(R.string.request_now,
+        builder.setPositiveButton(
+                R.string.request_now,
                 (dialog, which) -> {
                     if (xmppConnectionServiceBound) {
-                        xmppConnectionService.sendPresencePacket(contact
-                                .getAccount(), xmppConnectionService
-                                .getPresenceGenerator()
-                                .requestPresenceUpdatesFrom(contact));
+                        xmppConnectionService.sendPresencePacket(
+                                contact.getAccount(),
+                                xmppConnectionService
+                                        .getPresenceGenerator()
+                                        .requestPresenceUpdatesFrom(contact));
                     }
                 });
         builder.create().show();
@@ -782,7 +922,11 @@ public abstract class XmppActivity extends ActionBarActivity {
         quickEdit(previousValue, callback, hint, false, false);
     }
 
-    protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback, boolean permitEmpty) {
+    protected void quickEdit(
+            String previousValue,
+            @StringRes int hint,
+            OnValueEdited callback,
+            boolean permitEmpty) {
         quickEdit(previousValue, callback, hint, false, permitEmpty);
     }
 
@@ -791,15 +935,19 @@ public abstract class XmppActivity extends ActionBarActivity {
     }
 
     @SuppressLint("InflateParams")
-    private void quickEdit(final String previousValue,
-                           final OnValueEdited callback,
-                           final @StringRes int hint,
-                           boolean password,
-                           boolean permitEmpty) {
+    private void quickEdit(
+            final String previousValue,
+            final OnValueEdited callback,
+            final @StringRes int hint,
+            boolean password,
+            boolean permitEmpty) {
         final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
-        final DialogQuickeditBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_quickedit, null, false);
+        final DialogQuickeditBinding binding =
+                DataBindingUtil.inflate(
+                        getLayoutInflater(), R.layout.dialog_quickedit, null, false);
         if (password) {
-            binding.inputEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+            binding.inputEditText.setInputType(
+                    InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
         }
         builder.setPositiveButton(R.string.accept, null);
         if (hint != 0) {
@@ -814,33 +962,39 @@ public abstract class XmppActivity extends ActionBarActivity {
         final AlertDialog dialog = builder.create();
         dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(binding.inputEditText));
         dialog.show();
-        View.OnClickListener clickListener = v -> {
-            String value = binding.inputEditText.getText().toString();
-            if (!value.equals(previousValue) && (!value.trim().isEmpty() || permitEmpty)) {
-                String error = callback.onValueEdited(value);
-                if (error != null) {
-                    binding.inputLayout.setError(error);
-                    return;
-                }
-            }
-            SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
-            dialog.dismiss();
-        };
+        View.OnClickListener clickListener =
+                v -> {
+                    String value = binding.inputEditText.getText().toString();
+                    if (!value.equals(previousValue) && (!value.trim().isEmpty() || permitEmpty)) {
+                        String error = callback.onValueEdited(value);
+                        if (error != null) {
+                            binding.inputLayout.setError(error);
+                            return;
+                        }
+                    }
+                    SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
+                    dialog.dismiss();
+                };
         dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener);
-        dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v -> {
-            SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
-            dialog.dismiss();
-        }));
+        dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
+                .setOnClickListener(
+                        (v -> {
+                            SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
+                            dialog.dismiss();
+                        }));
         dialog.setCanceledOnTouchOutside(false);
-        dialog.setOnDismissListener(dialog1 -> {
-            SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
-        });
+        dialog.setOnDismissListener(
+                dialog1 -> {
+                    SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
+                });
     }
 
     protected boolean hasStoragePermission(int requestCode) {
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
-            if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
-                requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
+            if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+                    != PackageManager.PERMISSION_GRANTED) {
+                requestPermissions(
+                        new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
                 return false;
             } else {
                 return true;
@@ -876,7 +1030,8 @@ public abstract class XmppActivity extends ActionBarActivity {
     }
 
     protected boolean manuallyChangePresence() {
-        return getBooleanPreference(AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
+        return getBooleanPreference(
+                AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
     }
 
     protected String getShareableUri() {
@@ -906,16 +1061,21 @@ public abstract class XmppActivity extends ActionBarActivity {
         PgpEngine pgp = XmppActivity.this.xmppConnectionService.getPgpEngine();
         try {
             startIntentSenderForResult(
-                    pgp.getIntentForKey(keyId).getIntentSender(), 0, null, 0,
-                    0, 0, Compatibility.pgpStartIntentSenderOptions());
+                    pgp.getIntentForKey(keyId).getIntentSender(),
+                    0,
+                    null,
+                    0,
+                    0,
+                    0,
+                    Compatibility.pgpStartIntentSenderOptions());
         } catch (final Throwable e) {
-            Log.d(Config.LOGTAG,"could not launch OpenKeyChain", e);
+            Log.d(Config.LOGTAG, "could not launch OpenKeyChain", e);
             Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show();
         }
     }
 
     @Override
-    protected void onResume(){
+    protected void onResume() {
         super.onResume();
         SettingsUtils.applyScreenshotSetting(this);
     }
@@ -947,11 +1107,27 @@ public abstract class XmppActivity extends ActionBarActivity {
         final int black;
         final int white;
         if (Activities.isNightMode(this)) {
-            black = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceContainerHighest,"No surface color configured");
-            white = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceInverse,"No inverse surface color configured");
+            black =
+                    MaterialColors.getColor(
+                            this,
+                            com.google.android.material.R.attr.colorSurfaceContainerHighest,
+                            "No surface color configured");
+            white =
+                    MaterialColors.getColor(
+                            this,
+                            com.google.android.material.R.attr.colorSurfaceInverse,
+                            "No inverse surface color configured");
         } else {
-            black = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceInverse,"No inverse surface color configured");
-            white = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceContainerHighest,"No surface color configured");
+            black =
+                    MaterialColors.getColor(
+                            this,
+                            com.google.android.material.R.attr.colorSurfaceInverse,
+                            "No inverse surface color configured");
+            white =
+                    MaterialColors.getColor(
+                            this,
+                            com.google.android.material.R.attr.colorSurfaceContainerHighest,
+                            "No surface color configured");
         }
         final var bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width, black, white);
         final ImageView view = new ImageView(this);
@@ -978,7 +1154,10 @@ public abstract class XmppActivity extends ActionBarActivity {
     public void loadBitmap(Message message, ImageView imageView) {
         Bitmap bm;
         try {
-            bm = xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288), true);
+            bm =
+                    xmppConnectionService
+                            .getFileBackend()
+                            .getThumbnail(message, (int) (metrics.density * 288), true);
         } catch (IOException e) {
             bm = null;
         }
@@ -991,8 +1170,7 @@ public abstract class XmppActivity extends ActionBarActivity {
                 imageView.setBackgroundColor(0xff333333);
                 imageView.setImageDrawable(null);
                 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
-                final AsyncDrawable asyncDrawable = new AsyncDrawable(
-                        getResources(), null, task);
+                final AsyncDrawable asyncDrawable = new AsyncDrawable(getResources(), null, task);
                 imageView.setImageDrawable(asyncDrawable);
                 try {
                     task.execute(message);
@@ -1035,7 +1213,8 @@ public abstract class XmppActivity extends ActionBarActivity {
                 return false;
             } else {
                 jids.add(conversation.getJid().asBareJid());
-                return service.createAdhocConference(conversation.getAccount(), null, jids, activity.adhocCallback);
+                return service.createAdhocConference(
+                        conversation.getAccount(), null, jids, activity.adhocCallback);
             }
         }
     }
@@ -1057,7 +1236,9 @@ public abstract class XmppActivity extends ActionBarActivity {
             try {
                 final XmppActivity activity = find(imageViewReference);
                 if (activity != null && activity.xmppConnectionService != null) {
-                    return activity.xmppConnectionService.getFileBackend().getThumbnail(message, (int) (activity.metrics.density * 288), false);
+                    return activity.xmppConnectionService
+                            .getFileBackend()
+                            .getThumbnail(message, (int) (activity.metrics.density * 288), false);
                 } else {
                     return null;
                 }

src/main/res/menu/contact_details.xml 🔗

@@ -1,64 +1,70 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
-      xmlns:app="http://schemas.android.com/apk/res-auto">
+    xmlns:app="http://schemas.android.com/apk/res-auto">
 
     <item
         android:id="@+id/action_edit_contact"
         android:icon="@drawable/ic_edit_24dp"
         android:orderInCategory="10"
-        app:showAsAction="always"
-        android:title="@string/action_edit_contact"/>
+        android:title="@string/action_edit_contact"
+        app:showAsAction="always" />
 
     <item
         android:id="@+id/action_share"
         android:icon="@drawable/ic_share_24dp"
         android:orderInCategory="15"
-        app:showAsAction="always"
-        android:title="@string/share_uri_with">
+        android:title="@string/share_uri_with"
+        app:showAsAction="always">
         <menu>
             <item
                 android:id="@+id/action_share_uri"
-                android:title="@string/share_as_uri"/>
+                android:title="@string/share_as_uri" />
             <item
                 android:id="@+id/action_share_http"
-                android:title="@string/share_as_http"/>
+                android:title="@string/share_as_http" />
             <item
                 android:id="@+id/action_show_qr_code"
-                android:title="@string/show_qr_code"/>
+                android:title="@string/show_qr_code" />
         </menu>
     </item>
     <item
         android:id="@+id/action_delete_contact"
         android:orderInCategory="10"
-        app:showAsAction="never"
-        android:title="@string/action_delete_contact"/>
+        android:title="@string/action_delete_contact"
+        app:showAsAction="never" />
 
     <item
         android:id="@+id/action_block"
         android:orderInCategory="72"
-        app:showAsAction="never"
-        android:title="@string/action_block_contact"/>
+        android:title="@string/action_block_contact"
+        app:showAsAction="never" />
 
     <item
         android:id="@+id/action_unblock"
         android:orderInCategory="73"
-        app:showAsAction="never"
-        android:title="@string/action_unblock_contact"/>
+        android:title="@string/action_unblock_contact"
+        app:showAsAction="never" />
+
+    <item
+        android:id="@+id/action_custom_notifications"
+        android:orderInCategory="75"
+        android:title="@string/custom_notifications"
+        app:showAsAction="never" />
 
     <item
         android:id="@+id/action_accounts"
         android:orderInCategory="90"
-        app:showAsAction="never"
-        android:title="@string/action_accounts"/>
+        android:title="@string/action_accounts"
+        app:showAsAction="never" />
     <item
         android:id="@+id/action_account"
         android:orderInCategory="90"
         android:title="@string/action_account"
-        app:showAsAction="never"/>
+        app:showAsAction="never" />
     <item
         android:id="@+id/action_settings"
         android:orderInCategory="100"
-        app:showAsAction="never"
-        android:title="@string/action_settings"/>
+        android:title="@string/action_settings"
+        app:showAsAction="never" />
 
 </menu>

src/main/res/menu/muc_details.xml 🔗

@@ -1,57 +1,62 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
-      xmlns:app="http://schemas.android.com/apk/res-auto">
+    xmlns:app="http://schemas.android.com/apk/res-auto">
 
     <item
         android:id="@+id/action_share"
         android:icon="@drawable/ic_share_24dp"
         android:orderInCategory="15"
-        app:showAsAction="ifRoom"
-        android:title="@string/share_uri_with">
+        android:title="@string/share_uri_with"
+        app:showAsAction="ifRoom">
         <menu>
             <item
                 android:id="@+id/action_share_uri"
-                android:title="@string/share_as_uri"/>
+                android:title="@string/share_as_uri" />
             <item
                 android:id="@+id/action_share_http"
-                android:title="@string/share_as_http"/>
+                android:title="@string/share_as_http" />
             <item
                 android:id="@+id/action_show_qr_code"
-                android:title="@string/show_qr_code"/>
+                android:title="@string/show_qr_code" />
         </menu>
     </item>
 
     <item
         android:id="@+id/action_save_as_bookmark"
         android:orderInCategory="80"
-        app:showAsAction="never"
-        android:title="@string/save_as_bookmark"/>
+        android:title="@string/save_as_bookmark"
+        app:showAsAction="never" />
     <item
         android:id="@+id/action_destroy_room"
         android:orderInCategory="82"
-        app:showAsAction="never"
-        android:title="@string/destroy_room"/>
+        android:title="@string/destroy_room"
+        app:showAsAction="never" />
     <item
         android:id="@+id/action_advanced_mode"
         android:checkable="true"
         android:checked="false"
         android:orderInCategory="85"
-        app:showAsAction="never"
-        android:title="@string/advanced_mode"/>
+        android:title="@string/advanced_mode"
+        app:showAsAction="never" />
+    <item
+        android:id="@+id/action_custom_notifications"
+        android:orderInCategory="89"
+        android:title="@string/custom_notifications"
+        app:showAsAction="never" />
     <item
         android:id="@+id/action_accounts"
         android:orderInCategory="90"
-        app:showAsAction="never"
-        android:title="@string/action_accounts"/>
+        android:title="@string/action_accounts"
+        app:showAsAction="never" />
     <item
         android:id="@+id/action_account"
         android:orderInCategory="90"
         android:title="@string/action_account"
-        app:showAsAction="never"/>
+        app:showAsAction="never" />
     <item
         android:id="@+id/action_settings"
         android:orderInCategory="100"
-        app:showAsAction="never"
-        android:title="@string/action_settings"/>
+        android:title="@string/action_settings"
+        app:showAsAction="never" />
 
 </menu>

src/main/res/values/strings.xml 🔗

@@ -1097,4 +1097,6 @@
     <string name="pref_call_integration_summary">Calls from this app interact with regular phone calls, such as ending one call when another starts.</string>
     <string name="pref_align_start">Left-aligned messages</string>
     <string name="pref_align_start_summary">Display all messages, including sent ones, on the left side for a uniform chat layout.</string>
+    <string name="custom_notifications">Custom notifications</string>
+    <string name="custom_notifications_enable">Enable customized notification settings (importance, sound, vibration) settings for this conversation?</string>
 </resources>