ConversationsActivity.java

   1/*
   2 * Copyright (c) 2018, Daniel Gultsch All rights reserved.
   3 *
   4 * Redistribution and use in source and binary forms, with or without modification,
   5 * are permitted provided that the following conditions are met:
   6 *
   7 * 1. Redistributions of source code must retain the above copyright notice, this
   8 * list of conditions and the following disclaimer.
   9 *
  10 * 2. Redistributions in binary form must reproduce the above copyright notice,
  11 * this list of conditions and the following disclaimer in the documentation and/or
  12 * other materials provided with the distribution.
  13 *
  14 * 3. Neither the name of the copyright holder nor the names of its contributors
  15 * may be used to endorse or promote products derived from this software without
  16 * specific prior written permission.
  17 *
  18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  19 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
  22 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  25 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28 */
  29
  30package eu.siacs.conversations.ui;
  31
  32import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP;
  33
  34import android.Manifest;
  35import android.annotation.SuppressLint;
  36import android.app.Activity;
  37import android.app.Fragment;
  38import android.app.FragmentManager;
  39import android.app.FragmentTransaction;
  40import android.content.ActivityNotFoundException;
  41import android.content.ComponentName;
  42import android.content.Context;
  43import android.content.Intent;
  44import android.content.pm.PackageManager;
  45import android.graphics.Bitmap;
  46import android.net.Uri;
  47import android.os.Build;
  48import android.os.Bundle;
  49import android.provider.Settings;
  50import android.util.Log;
  51import android.util.Pair;
  52import android.view.KeyEvent;
  53import android.view.Menu;
  54import android.view.MenuItem;
  55import android.widget.Toast;
  56import androidx.annotation.IdRes;
  57import androidx.annotation.NonNull;
  58import androidx.appcompat.app.ActionBar;
  59import androidx.appcompat.app.AlertDialog;
  60import androidx.core.app.ActivityCompat;
  61import androidx.core.content.ContextCompat;
  62import androidx.databinding.DataBindingUtil;
  63
  64import com.cheogram.android.DownloadDefaultStickers;
  65import com.cheogram.android.FinishOnboarding;
  66
  67import com.google.common.collect.ImmutableList;
  68
  69import io.michaelrocks.libphonenumber.android.NumberParseException;
  70import com.google.android.material.dialog.MaterialAlertDialogBuilder;
  71import com.google.android.material.color.MaterialColors;
  72
  73import org.jetbrains.annotations.NotNull;
  74import org.openintents.openpgp.util.OpenPgpApi;
  75
  76import java.util.Arrays;
  77import java.util.ArrayList;
  78import java.util.HashSet;
  79import java.util.HashMap;
  80import java.util.List;
  81import java.util.Objects;
  82import java.util.Set;
  83import java.util.TreeMap;
  84import java.util.concurrent.RejectedExecutionException;
  85import java.util.concurrent.atomic.AtomicBoolean;
  86import java.util.stream.Collectors;
  87import java.util.stream.Stream;
  88
  89import eu.siacs.conversations.Config;
  90import eu.siacs.conversations.R;
  91import eu.siacs.conversations.crypto.OmemoSetting;
  92import eu.siacs.conversations.databinding.ActivityConversationsBinding;
  93import eu.siacs.conversations.entities.Account;
  94import eu.siacs.conversations.entities.Contact;
  95import eu.siacs.conversations.entities.Conversation;
  96import eu.siacs.conversations.entities.Conversational;
  97import eu.siacs.conversations.entities.ListItem.Tag;
  98import eu.siacs.conversations.persistance.FileBackend;
  99import eu.siacs.conversations.services.XmppConnectionService;
 100import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
 101import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
 102import eu.siacs.conversations.ui.interfaces.OnConversationRead;
 103import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
 104import eu.siacs.conversations.ui.interfaces.OnConversationsListItemUpdated;
 105import eu.siacs.conversations.ui.util.ActivityResult;
 106import eu.siacs.conversations.ui.util.AvatarWorkerTask;
 107import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
 108import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 109import eu.siacs.conversations.ui.util.PendingItem;
 110import eu.siacs.conversations.ui.util.ToolbarUtils;
 111import eu.siacs.conversations.ui.util.SendButtonTool;
 112import eu.siacs.conversations.utils.AccountUtils;
 113import eu.siacs.conversations.utils.ExceptionHelper;
 114import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
 115import eu.siacs.conversations.utils.SignupUtils;
 116import eu.siacs.conversations.utils.ThemeHelper;
 117import eu.siacs.conversations.utils.XmppUri;
 118import eu.siacs.conversations.xmpp.Jid;
 119import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 120import java.util.Arrays;
 121import java.util.List;
 122import java.util.concurrent.atomic.AtomicBoolean;
 123import org.openintents.openpgp.util.OpenPgpApi;
 124
 125public class ConversationsActivity extends XmppActivity
 126        implements OnConversationSelected,
 127                OnConversationArchived,
 128                OnConversationsListItemUpdated,
 129                OnConversationRead,
 130                XmppConnectionService.OnAccountUpdate,
 131                XmppConnectionService.OnConversationUpdate,
 132                XmppConnectionService.OnRosterUpdate,
 133                OnUpdateBlocklist,
 134                XmppConnectionService.OnShowErrorToast,
 135                XmppConnectionService.OnAffiliationChanged {
 136
 137    public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW";
 138    public static final String EXTRA_CONVERSATION = "conversationUuid";
 139    public static final String EXTRA_DOWNLOAD_UUID = "eu.siacs.conversations.download_uuid";
 140    public static final String EXTRA_AS_QUOTE = "eu.siacs.conversations.as_quote";
 141    public static final String EXTRA_NICK = "nick";
 142    public static final String EXTRA_IS_PRIVATE_MESSAGE = "pm";
 143    public static final String EXTRA_DO_NOT_APPEND = "do_not_append";
 144    public static final String EXTRA_POST_INIT_ACTION = "post_init_action";
 145    public static final String POST_ACTION_RECORD_VOICE = "record_voice";
 146    public static final String EXTRA_THREAD = "threadId";
 147    public static final String EXTRA_TYPE = "type";
 148    public static final String EXTRA_NODE = "node";
 149    public static final String EXTRA_JID = "jid";
 150
 151    private static final List<String> VIEW_AND_SHARE_ACTIONS =
 152            Arrays.asList(
 153                    ACTION_VIEW_CONVERSATION, Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE);
 154
 155    public static final int REQUEST_OPEN_MESSAGE = 0x9876;
 156    public static final int REQUEST_PLAY_PAUSE = 0x5432;
 157    public static final int REQUEST_MICROPHONE = 0x5432f;
 158    public static final int DIALLER_INTEGRATION = 0x5432ff;
 159    public static final int REQUEST_DOWNLOAD_STICKERS = 0xbf8702;
 160
 161    public static final long DRAWER_ALL_CHATS = 1;
 162    public static final long DRAWER_UNREAD_CHATS = 2;
 163    public static final long DRAWER_DIRECT_MESSAGES = 3;
 164    public static final long DRAWER_MANAGE_ACCOUNT = 4;
 165    public static final long DRAWER_ADD_ACCOUNT = 5;
 166    public static final long DRAWER_MANAGE_PHONE_ACCOUNTS = 6;
 167    public static final long DRAWER_CHANNELS = 7;
 168    public static final long DRAWER_CHAT_REQUESTS = 8;
 169    public static final long DRAWER_SETTINGS = 9;
 170    public static final long DRAWER_START_CHAT = 10;
 171    public static final long DRAWER_START_CHAT_CONTACT = 11;
 172    public static final long DRAWER_START_CHAT_NEW = 12;
 173    public static final long DRAWER_START_CHAT_GROUP = 13;
 174    public static final long DRAWER_START_CHAT_PUBLIC = 14;
 175    public static final long DRAWER_START_CHAT_DISCOVER = 15;
 176
 177    // secondary fragment (when holding the conversation, must be initialized before refreshing the
 178    // overview fragment
 179    private static final @IdRes int[] FRAGMENT_ID_NOTIFICATION_ORDER = {
 180        R.id.secondary_fragment, R.id.main_fragment
 181    };
 182    private final PendingItem<Intent> pendingViewIntent = new PendingItem<>();
 183    private final PendingItem<ActivityResult> postponedActivityResult = new PendingItem<>();
 184    private ActivityConversationsBinding binding;
 185    private boolean mActivityPaused = true;
 186    private final AtomicBoolean mRedirectInProcess = new AtomicBoolean(false);
 187    private boolean refreshForNewCaps = false;
 188    private Set<Jid> newCapsJids = new HashSet<>();
 189    private int mRequestCode = -1;
 190    private com.mikepenz.materialdrawer.widget.AccountHeaderView accountHeader;
 191    private Bundle savedState = null;
 192    private HashSet<Tag> selectedTag = new HashSet<>();
 193    private long mainFilter = DRAWER_ALL_CHATS;
 194    private boolean refreshAccounts = true;
 195
 196    private static boolean isViewOrShareIntent(Intent i) {
 197        Log.d(Config.LOGTAG, "action: " + (i == null ? null : i.getAction()));
 198        return i != null
 199                && VIEW_AND_SHARE_ACTIONS.contains(i.getAction())
 200                && i.hasExtra(EXTRA_CONVERSATION);
 201    }
 202
 203    private static Intent createLauncherIntent(Context context) {
 204        final Intent intent = new Intent(context, ConversationsActivity.class);
 205        intent.setAction(Intent.ACTION_MAIN);
 206        intent.addCategory(Intent.CATEGORY_LAUNCHER);
 207        return intent;
 208    }
 209
 210    @Override
 211    protected void refreshUiReal() {
 212        invalidateOptionsMenu();
 213        for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) {
 214            refreshFragment(id);
 215        }
 216        refreshForNewCaps = false;
 217        newCapsJids.clear();
 218
 219        if (accountHeader == null) return;
 220
 221        final var chatRequestsPref = xmppConnectionService.getStringPreference("chat_requests", R.string.default_chat_requests);
 222        final var accountUnreads = new HashMap<Account, Integer>();
 223        binding.drawer.apply(dr -> {
 224            final var items = binding.drawer.getItemAdapter().getAdapterItems();
 225            final var tags = new TreeMap<Tag, Integer>();
 226            final var conversations = new ArrayList<Conversation>();
 227            final var removedConversations = new ArrayList<Conversation>();
 228            var totalUnread = 0;
 229            var dmUnread = 0;
 230            var channelUnread = 0;
 231            var chatRequests = 0;
 232            final var selectedAccount = selectedAccount();
 233            // What sort of memory footprint does this function typically have?
 234            populateWithOrderedConversations(
 235                    conversations,
 236                    removedConversations,
 237                    false
 238            );
 239
 240            final var invisibles = removedConversations.stream().anyMatch(c -> c.unreadCount(xmppConnectionService) > 0);
 241
 242            // Reconstruct the complete list for counts and tags computation
 243            conversations.addAll(removedConversations);
 244
 245            for (final var c : conversations) {
 246                final var unread = c.unreadCount(xmppConnectionService);
 247                if (selectedAccount == null || selectedAccount.getUuid().equals(c.getAccount().getUuid())) {
 248                    if (c.isChatRequest(chatRequestsPref)) {
 249                        chatRequests++;
 250                    } else {
 251                        totalUnread += unread;
 252                        if (c.getMode() == Conversation.MODE_MULTI) {
 253                            channelUnread += unread;
 254                        } else {
 255                            dmUnread += unread;
 256                        }
 257                    }
 258                }
 259                var accountUnread = accountUnreads.get(c.getAccount());
 260                if (accountUnread == null) accountUnread = 0;
 261                accountUnreads.put(c.getAccount(), accountUnread + unread);
 262            }
 263
 264            filterByMainFilter(conversations);
 265            for (final var c : conversations) {
 266                if (selectedAccount == null || selectedAccount.getUuid().equals(c.getAccount().getUuid())) {
 267                    final var unread = c.unreadCount(xmppConnectionService);
 268                    for (final var tag : c.getTags(this)) {
 269                        if ("Channel".equals(tag.getName())) continue;
 270                        var count = tags.get(tag);
 271                        if (count == null) count = 0;
 272                        tags.put(tag, count + unread);
 273                    }
 274                }
 275            }
 276
 277            ActionBar supportBar = getSupportActionBar();
 278
 279            if (invisibles && supportBar != null) {
 280               supportBar.setHomeAsUpIndicator(R.drawable.menu_with_dot_24dp);
 281            } else if (supportBar != null) {
 282                supportBar.setHomeAsUpIndicator(R.drawable.menu_24dp);
 283            }
 284
 285            com.mikepenz.materialdrawer.util.MaterialDrawerSliderViewExtensionsKt.updateBadge(
 286                binding.drawer,
 287                DRAWER_UNREAD_CHATS,
 288                new com.mikepenz.materialdrawer.holder.StringHolder(totalUnread > 0 ? new Integer(totalUnread).toString() : null)
 289            );
 290
 291            com.mikepenz.materialdrawer.util.MaterialDrawerSliderViewExtensionsKt.updateBadge(
 292                binding.drawer,
 293                DRAWER_DIRECT_MESSAGES,
 294                new com.mikepenz.materialdrawer.holder.StringHolder(dmUnread > 0 ? new Integer(dmUnread).toString() : null)
 295            );
 296
 297            com.mikepenz.materialdrawer.util.MaterialDrawerSliderViewExtensionsKt.updateBadge(
 298                binding.drawer,
 299                DRAWER_CHANNELS,
 300                new com.mikepenz.materialdrawer.holder.StringHolder(channelUnread > 0 ? new Integer(channelUnread).toString() : null)
 301            );
 302
 303            if (chatRequests > 0) {
 304                if (binding.drawer.getItemAdapter().getAdapterPosition(DRAWER_CHAT_REQUESTS) < 0) {
 305                    final var color = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorPrimaryContainer);
 306                    final var textColor = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorOnPrimaryContainer);
 307                    final var requests = new com.mikepenz.materialdrawer.model.PrimaryDrawerItem();
 308                    requests.setIdentifier(DRAWER_CHAT_REQUESTS);
 309                    com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(requests, "Chat Requests");
 310                    com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(requests, R.drawable.ic_person_add_24dp);
 311                    requests.setBadgeStyle(new com.mikepenz.materialdrawer.holder.BadgeStyle(com.mikepenz.materialdrawer.R.drawable.material_drawer_badge, color, color, textColor));
 312                    binding.drawer.getItemAdapter().add(binding.drawer.getItemAdapter().getGlobalPosition(binding.drawer.getItemAdapter().getAdapterPosition(DRAWER_CHANNELS) + 1), requests);
 313                }
 314                com.mikepenz.materialdrawer.util.MaterialDrawerSliderViewExtensionsKt.updateBadge(
 315                    binding.drawer,
 316                    DRAWER_CHAT_REQUESTS,
 317                    new com.mikepenz.materialdrawer.holder.StringHolder(chatRequests > 0 ? new Integer(chatRequests).toString() : null)
 318                );
 319            } else {
 320                binding.drawer.getItemAdapter().removeByIdentifier(DRAWER_CHAT_REQUESTS);
 321            }
 322
 323            final var endOfMainFilters = chatRequests > 0 ? 6 : 5;
 324            long id = 1000;
 325            final var inDrawer = new HashMap<Tag, Long>();
 326            for (final var item : ImmutableList.copyOf(items)) {
 327                if (item.getIdentifier() >= 1000 && !tags.containsKey(item.getTag())) {
 328                    com.mikepenz.materialdrawer.util.MaterialDrawerSliderViewExtensionsKt.removeItems(binding.drawer, item);
 329                } else if (item.getIdentifier() >= 1000) {
 330                    inDrawer.put((Tag)item.getTag(), item.getIdentifier());
 331                    id = item.getIdentifier() + 1;
 332                }
 333            }
 334
 335            for (final var entry : tags.entrySet()) {
 336                final var badge = entry.getValue() > 0 ? entry.getValue().toString() : null;
 337                if (inDrawer.containsKey(entry.getKey())) {
 338                    com.mikepenz.materialdrawer.util.MaterialDrawerSliderViewExtensionsKt.updateBadge(
 339                        binding.drawer,
 340                        inDrawer.get(entry.getKey()),
 341                        new com.mikepenz.materialdrawer.holder.StringHolder(badge)
 342                    );
 343                } else {
 344                    final var item = new com.mikepenz.materialdrawer.model.SecondaryDrawerItem();
 345                    item.setIdentifier(id++);
 346                    item.setTag(entry.getKey());
 347                    com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(item, entry.getKey().getName());
 348                    if (badge != null) com.mikepenz.materialdrawer.model.interfaces.BadgeableKt.setBadgeText(item, badge);
 349                    final var color = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorPrimaryContainer);
 350                    final var textColor = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorOnPrimaryContainer);
 351                    item.setBadgeStyle(new com.mikepenz.materialdrawer.holder.BadgeStyle(com.mikepenz.materialdrawer.R.drawable.material_drawer_badge, color, color, textColor));
 352                    binding.drawer.getItemAdapter().add(binding.drawer.getItemAdapter().getGlobalPosition(endOfMainFilters), item);
 353                }
 354            }
 355
 356            items.subList(endOfMainFilters, Math.min(endOfMainFilters + tags.size(), items.size())).sort((x, y) -> x.getTag() == null ? -1 : ((Comparable) x.getTag()).compareTo(y.getTag()));
 357            binding.drawer.getItemAdapter().getFastAdapter().notifyDataSetChanged();
 358            return kotlin.Unit.INSTANCE;
 359        });
 360
 361
 362        accountHeader.apply(ah -> {
 363            //if (!refreshAccounts) return kotlin.Unit.INSTANCE;
 364            refreshAccounts = false;
 365            final var accounts = xmppConnectionService.getAccounts();
 366            final var inHeader = new HashMap<Account, com.mikepenz.materialdrawer.model.ProfileDrawerItem>();
 367            long id = 101;
 368            for (final var p : ImmutableList.copyOf(accountHeader.getProfiles())) {
 369                if (p instanceof com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem) continue;
 370                final var pId = p.getIdentifier();
 371                if (id < pId) id = pId;
 372                if (accounts.contains(p.getTag()) || (accounts.size() > 1 && p.getTag() == null)) {
 373                    inHeader.put((Account) p.getTag(), (com.mikepenz.materialdrawer.model.ProfileDrawerItem) p);
 374                } else {
 375                    accountHeader.removeProfile(p);
 376                }
 377            }
 378
 379            if (accounts.size() > 1 && !inHeader.containsKey(null)) {
 380                final var all = new com.mikepenz.materialdrawer.model.ProfileDrawerItem();
 381                all.setIdentifier(100);
 382                com.mikepenz.materialdrawer.model.interfaces.DescribableKt.setDescriptionText(all, "All Accounts");
 383                com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(all, R.drawable.main_logo);
 384                accountHeader.addProfile(all, 0);
 385            }
 386
 387            accountHeader.removeProfileByIdentifier(DRAWER_MANAGE_PHONE_ACCOUNTS);
 388            final var hasPhoneAccounts = accounts.stream().anyMatch(a -> a.getGateways("pstn").size() > 0);
 389            if (hasPhoneAccounts) {
 390                final var phoneAccounts = new com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem();
 391                phoneAccounts.setIdentifier(DRAWER_MANAGE_PHONE_ACCOUNTS);
 392                com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(phoneAccounts, "Manage Phone Accounts");
 393                com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(phoneAccounts, R.drawable.ic_call_24dp);
 394                accountHeader.addProfile(phoneAccounts, accountHeader.getProfiles().size() - 2);
 395            }
 396
 397            final boolean nightMode = Activities.isNightMode(this);
 398            for (final var a : accounts) {
 399                final var size = (int) getResources().getDimension(R.dimen.avatar_on_drawer);
 400                final var avatar = xmppConnectionService.getAvatarService().get(a, size, true);
 401                if (avatar == null) {
 402                    final var task = new AvatarWorkerTask(this, R.dimen.avatar_on_drawer);
 403                    try { task.execute(a); } catch (final RejectedExecutionException ignored) { }
 404                    refreshAccounts = true;
 405                }
 406                final var alreadyInHeader = inHeader.get(a);
 407                final com.mikepenz.materialdrawer.model.ProfileDrawerItem p;
 408                if (alreadyInHeader == null) {
 409                    p = new com.mikepenz.materialdrawer.model.ProfileDrawerItem();
 410                    p.setIdentifier(id++);
 411                    p.setTag(a);
 412                } else {
 413                    p = alreadyInHeader;
 414                }
 415                com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(p, a.getDisplayName() == null ? "" : a.getDisplayName());
 416                com.mikepenz.materialdrawer.model.interfaces.DescribableKt.setDescriptionText(p, a.getJid().asBareJid().toString());
 417                if (avatar != null) com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconBitmap(p, FileBackend.drawDrawable(avatar).copy(Bitmap.Config.ARGB_8888, false));
 418                var color = SendButtonTool.getSendButtonColor(binding.drawer, a.getPresenceStatus());
 419                if (!a.isOnlineAndConnected()) {
 420                    if (a.getStatus().isError()) {
 421                        color = MaterialColors.harmonizeWithPrimary(this, ContextCompat.getColor(this, nightMode ? R.color.red_300 : R.color.red_800));
 422                    } else {
 423                        color = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorOnSurface);
 424                    }
 425                }
 426                final var textColor = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorOnPrimary);
 427                p.setBadgeStyle(new com.mikepenz.materialdrawer.holder.BadgeStyle(com.mikepenz.materialdrawer.R.drawable.material_drawer_badge, color, color, textColor));
 428                final var badgeNumber = accountUnreads.get(a);
 429                p.setBadge(new com.mikepenz.materialdrawer.holder.StringHolder(badgeNumber == null || badgeNumber < 1 ? " " : badgeNumber.toString()));
 430                if (alreadyInHeader == null) {
 431                    accountHeader.addProfile(p, accountHeader.getProfiles().size() - (hasPhoneAccounts ? 3 : 2));
 432                } else {
 433                    accountHeader.updateProfile(p);
 434                }
 435            }
 436            return kotlin.Unit.INSTANCE;
 437        });
 438    }
 439
 440    @Override
 441    protected void onBackendConnected() {
 442        final var useSavedState = savedState;
 443        savedState = null;
 444        if (performRedirectIfNecessary(true)) {
 445            return;
 446        }
 447        xmppConnectionService.getNotificationService().setIsInForeground(true);
 448        var intent = pendingViewIntent.pop();
 449        if (intent != null) {
 450            if (processViewIntent(intent)) {
 451                if (binding.secondaryFragment != null) {
 452                    notifyFragmentOfBackendConnected(R.id.main_fragment);
 453                }
 454            } else {
 455                intent = null;
 456            }
 457        }
 458
 459        if (intent == null) {
 460            for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) {
 461                notifyFragmentOfBackendConnected(id);
 462            }
 463
 464            final ActivityResult activityResult = postponedActivityResult.pop();
 465            if (activityResult != null) {
 466                handleActivityResult(activityResult);
 467            }
 468            if (binding.secondaryFragment != null
 469                    && ConversationFragment.getConversation(this) == null) {
 470                Conversation conversation = ConversationsOverviewFragment.getSuggestion(this);
 471                if (conversation != null) {
 472                    openConversation(conversation, null);
 473                }
 474            }
 475            showDialogsIfMainIsOverview();
 476        }
 477        invalidateActionBarTitle();
 478
 479        if (accountHeader != null || binding == null || binding.drawer == null) {
 480            refreshUiReal();
 481            return;
 482        }
 483
 484        accountHeader = new com.mikepenz.materialdrawer.widget.AccountHeaderView(this);
 485        final var manageAccount = new com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem();
 486        manageAccount.setIdentifier(DRAWER_MANAGE_ACCOUNT);
 487        com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(manageAccount, getResources().getString(R.string.action_accounts));
 488        com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(manageAccount, R.drawable.ic_settings_24dp);
 489        accountHeader.addProfiles(manageAccount);
 490
 491        final var addAccount = new com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem();
 492        addAccount.setIdentifier(DRAWER_ADD_ACCOUNT);
 493        com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(addAccount, getResources().getString(R.string.action_add_account));
 494        com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(addAccount, R.drawable.ic_add_24dp);
 495        accountHeader.addProfiles(addAccount);
 496
 497        final var color = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorPrimaryContainer);
 498        final var textColor = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorOnPrimaryContainer);
 499
 500        final var allChats = new com.mikepenz.materialdrawer.model.PrimaryDrawerItem();
 501        allChats.setIdentifier(DRAWER_ALL_CHATS);
 502        com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(allChats, "All Chats");
 503        com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(allChats, R.drawable.ic_chat_24dp);
 504
 505        final var unreadChats = new com.mikepenz.materialdrawer.model.PrimaryDrawerItem();
 506        unreadChats.setIdentifier(DRAWER_UNREAD_CHATS);
 507        com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(unreadChats, "Unread Chats");
 508        com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(unreadChats, R.drawable.chat_unread_24dp);
 509        unreadChats.setBadgeStyle(new com.mikepenz.materialdrawer.holder.BadgeStyle(com.mikepenz.materialdrawer.R.drawable.material_drawer_badge, color, color, textColor));
 510
 511        final var directMessages = new com.mikepenz.materialdrawer.model.PrimaryDrawerItem();
 512        directMessages.setIdentifier(DRAWER_DIRECT_MESSAGES);
 513        com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(directMessages, "Direct Messages");
 514        com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(directMessages, R.drawable.ic_person_24dp);
 515        directMessages.setBadgeStyle(new com.mikepenz.materialdrawer.holder.BadgeStyle(com.mikepenz.materialdrawer.R.drawable.material_drawer_badge, color, color, textColor));
 516
 517        final var channels = new com.mikepenz.materialdrawer.model.PrimaryDrawerItem();
 518        channels.setIdentifier(DRAWER_CHANNELS);
 519        com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(channels, "Channels");
 520        com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(channels, R.drawable.ic_group_24dp);
 521        channels.setBadgeStyle(new com.mikepenz.materialdrawer.holder.BadgeStyle(com.mikepenz.materialdrawer.R.drawable.material_drawer_badge, color, color, textColor));
 522
 523        binding.drawer.getItemAdapter().add(
 524            allChats,
 525            unreadChats,
 526            directMessages,
 527            channels,
 528            new com.mikepenz.materialdrawer.model.DividerDrawerItem()
 529        );
 530
 531        final var settings = new com.mikepenz.materialdrawer.model.PrimaryDrawerItem();
 532        settings.setIdentifier(DRAWER_SETTINGS);
 533        settings.setSelectable(false);
 534        com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(settings, "Settings");
 535        com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(settings, R.drawable.ic_settings_24dp);
 536        com.mikepenz.materialdrawer.util.MaterialDrawerSliderViewExtensionsKt.addStickyDrawerItems(binding.drawer, settings);
 537
 538        if (useSavedState != null) {
 539            mainFilter = useSavedState.getLong("mainFilter", DRAWER_ALL_CHATS);
 540            selectedTag = (HashSet<Tag>) useSavedState.getSerializable("selectedTag");
 541        }
 542        refreshUiReal();
 543        if (useSavedState != null) binding.drawer.setSavedInstance(useSavedState);
 544        accountHeader.attachToSliderView(binding.drawer);
 545        if (useSavedState != null) accountHeader.withSavedInstance(useSavedState);
 546
 547        if (mainFilter == DRAWER_ALL_CHATS && selectedTag.isEmpty()) {
 548            binding.drawer.setSelectedItemIdentifier(DRAWER_ALL_CHATS);
 549        }
 550
 551        binding.drawer.setOnDrawerItemClickListener((v, drawerItem, pos) -> {
 552            final var id = drawerItem.getIdentifier();
 553            if (id != DRAWER_START_CHAT) binding.drawer.getExpandableExtension().collapse(false);
 554            if (id == DRAWER_SETTINGS) {
 555                startActivity(new Intent(this, eu.siacs.conversations.ui.activity.SettingsActivity.class));
 556                return false;
 557            } else if (id == DRAWER_START_CHAT_CONTACT) {
 558                launchStartConversation();
 559            } else if (id == DRAWER_START_CHAT_NEW) {
 560                launchStartConversation(R.id.create_contact);
 561            } else if (id == DRAWER_START_CHAT_GROUP) {
 562                launchStartConversation(R.id.create_private_group_chat);
 563            } else if (id == DRAWER_START_CHAT_PUBLIC) {
 564                launchStartConversation(R.id.create_public_channel);
 565            } else if (id == DRAWER_START_CHAT_DISCOVER) {
 566                launchStartConversation(R.id.discover_public_channels);
 567            } else if (id == DRAWER_ALL_CHATS || id == DRAWER_UNREAD_CHATS || id == DRAWER_DIRECT_MESSAGES || id == DRAWER_CHANNELS || id == DRAWER_CHAT_REQUESTS) {
 568                selectedTag.clear();
 569                mainFilter = id;
 570                binding.drawer.getSelectExtension().deselect();
 571            } else if (id >= 1000) {
 572                selectedTag.clear();
 573                selectedTag.add((Tag) drawerItem.getTag());
 574                binding.drawer.getSelectExtension().deselect();
 575                binding.drawer.getSelectExtension().select(pos, false, true);
 576            }
 577            binding.drawer.getSelectExtension().selectByIdentifier(mainFilter, false, true);
 578
 579            final var fm = getFragmentManager();
 580            while (fm.getBackStackEntryCount() > 0) {
 581                try {
 582                    fm.popBackStackImmediate();
 583                } catch (IllegalStateException e) {
 584                    break;
 585                }
 586            }
 587
 588            refreshUi();
 589            return false;
 590        });
 591
 592        binding.drawer.setOnDrawerItemLongClickListener((v, drawerItem, pos) -> {
 593            final var id = drawerItem.getIdentifier();
 594            if (id == DRAWER_ALL_CHATS || id == DRAWER_UNREAD_CHATS || id == DRAWER_DIRECT_MESSAGES || id == DRAWER_CHANNELS || id == DRAWER_CHAT_REQUESTS) {
 595                selectedTag.clear();
 596                mainFilter = id;
 597                binding.drawer.getSelectExtension().deselect();
 598            } else if (id >= 1000) {
 599                final var tag = (Tag) drawerItem.getTag();
 600                if (selectedTag.contains(tag)) {
 601                    selectedTag.remove(tag);
 602                    binding.drawer.getSelectExtension().deselect(pos);
 603                } else {
 604                    selectedTag.add(tag);
 605                    binding.drawer.getSelectExtension().select(pos, false, true);
 606                }
 607            }
 608            binding.drawer.getSelectExtension().selectByIdentifier(mainFilter, false, true);
 609
 610            refreshUi();
 611            return true;
 612        });
 613
 614         accountHeader.setOnAccountHeaderListener((v, profile, isCurrent) -> {
 615            final var id = profile.getIdentifier();
 616            if (isCurrent) return false; // Ignore switching to already selected profile
 617
 618            if (id == DRAWER_MANAGE_ACCOUNT) {
 619                AccountUtils.launchManageAccounts(this);
 620                return false;
 621            }
 622
 623            if (id == DRAWER_ADD_ACCOUNT) {
 624                startActivity(new Intent(this, EditAccountActivity.class));
 625                return false;
 626            }
 627
 628            if (id == DRAWER_MANAGE_PHONE_ACCOUNTS) {
 629                final String[] permissions;
 630                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
 631                    permissions = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.BLUETOOTH_CONNECT};
 632                } else {
 633                    permissions = new String[]{Manifest.permission.RECORD_AUDIO};
 634                }
 635                requestPermissions(permissions, REQUEST_MICROPHONE);
 636                return false;
 637            }
 638
 639            final var fm = getFragmentManager();
 640            while (fm.getBackStackEntryCount() > 0) {
 641                try {
 642                    fm.popBackStackImmediate();
 643                } catch (IllegalStateException e) {
 644                    break;
 645                }
 646            }
 647
 648            refreshUi();
 649
 650            return false;
 651        });
 652
 653         accountHeader.setOnAccountHeaderProfileImageListener((v, profile, isCurrent) -> {
 654            if (isCurrent) {
 655                final Account account = (Account) accountHeader.getActiveProfile().getTag();
 656                if (account == null) {
 657                    AccountUtils.launchManageAccounts(this);
 658                } else {
 659                    switchToAccount(account);
 660                }
 661            }
 662            return false;
 663         });
 664    }
 665
 666    @Override
 667    public boolean colorCodeAccounts() {
 668        if (accountHeader != null) {
 669            final var active = accountHeader.getActiveProfile();
 670            if (active != null && active.getTag() != null) return false;
 671        }
 672        return super.colorCodeAccounts();
 673    }
 674
 675    @Override
 676    public void populateWithOrderedConversations(List<Conversation> list) {
 677        populateWithOrderedConversations(list, new ArrayList<>(), true);
 678    }
 679
 680    public void populateWithOrderedConversations(
 681            @NotNull List<Conversation> list,
 682            final boolean sort
 683    ) {
 684        populateWithOrderedConversations(list, new ArrayList<>(), sort);
 685    }
 686
 687    // @param conversations an initially empty list representing the messages
 688    //         to returned as relevant
 689    // @return a `boolean` value indicating whether any conversations
 690    //         were filtered out and therefore would be invisible to
 691    //         the user.
 692    public void populateWithOrderedConversations(
 693            @NotNull List<Conversation> retained,
 694            @NotNull List<Conversation> removed,
 695            final boolean sort
 696    ) {
 697		if (sort) {
 698			super.populateWithOrderedConversations(retained);
 699		} else {
 700			retained.addAll(xmppConnectionService.getConversations());
 701		}
 702
 703		filterByMainFilter(retained, removed);
 704
 705		final var selectedAccount = selectedAccount();
 706
 707		for (final var c : ImmutableList.copyOf(retained)) {
 708			if (selectedAccount != null && !selectedAccount.getUuid().equals(c.getAccount().getUuid())) {
 709				retained.remove(c);
 710				removed.add(c);
 711			} else if (!selectedTag.isEmpty()) {
 712				final var tags = new HashSet<>(c.getTags(this));
 713				tags.retainAll(selectedTag);
 714				if (tags.isEmpty()) {
 715					retained.remove(c);
 716					removed.add(c);
 717				}
 718			}
 719		}
 720    }
 721
 722    protected Account selectedAccount() {
 723        if (accountHeader == null || accountHeader.getActiveProfile() == null) return null;
 724        return (Account) accountHeader.getActiveProfile().getTag();
 725    }
 726
 727    // Applies pre-defined filters to specify that only conversations
 728    // from certain accounts or types of conversations are displayed.
 729    //
 730    // @param `retained` an initially-empty list representing the messages
 731    //         to eventually be returned as relevant.
 732    // @param `removed` an initially-empty list representing the messages
 733    //         filtered out by tags and `filterByMainFilter`
 734    protected void filterByMainFilter(
 735        @NotNull List<Conversation> list,
 736        @NotNull List<Conversation> removed
 737    ) {
 738         final var chatRequests = xmppConnectionService.getStringPreference("chat_requests", R.string.default_chat_requests);
 739         for (final var c : ImmutableList.copyOf(list)) {
 740            if (mainFilter == DRAWER_CHANNELS && c.getMode() != Conversation.MODE_MULTI) {
 741                list.remove(c);
 742                removed.add(c);
 743            } else if (mainFilter == DRAWER_DIRECT_MESSAGES && c.getMode() == Conversation.MODE_MULTI) {
 744                list.remove(c);
 745                removed.add(c);
 746            } else if (mainFilter == DRAWER_UNREAD_CHATS && c.unreadCount(xmppConnectionService) < 1) {
 747                list.remove(c);
 748                removed.add(c);
 749            } else if (mainFilter == DRAWER_CHAT_REQUESTS && !c.isChatRequest(chatRequests)) {
 750                list.remove(c);
 751                removed.add(c);
 752            }
 753            if (mainFilter != DRAWER_CHAT_REQUESTS && c.isChatRequest(chatRequests)) {
 754                list.remove(c);
 755                removed.add(c);
 756            }
 757        }
 758    }
 759
 760    protected void filterByMainFilter(@NotNull List<Conversation> list) {
 761        filterByMainFilter(list, new ArrayList<>());
 762    }
 763
 764    @Override
 765    public void launchStartConversation() {
 766        launchStartConversation(0);
 767    }
 768
 769    public void launchStartConversation(int goTo) {
 770        StartConversationActivity.launch(this, accountHeader == null ? null : (Account) accountHeader.getActiveProfile().getTag(), selectedTag.stream().map(tag -> tag.getName()).collect(Collectors.joining(", ")), goTo);
 771    }
 772
 773    private boolean performRedirectIfNecessary(boolean noAnimation) {
 774        return performRedirectIfNecessary(null, noAnimation);
 775    }
 776
 777    private boolean performRedirectIfNecessary(
 778            final Conversation ignore, final boolean noAnimation) {
 779        if (xmppConnectionService == null) {
 780            return false;
 781        }
 782
 783        boolean isConversationsListEmpty = xmppConnectionService.isConversationsListEmpty(ignore);
 784        if (isConversationsListEmpty && mRedirectInProcess.compareAndSet(false, true)) {
 785            final Intent intent = SignupUtils.getRedirectionIntent(this);
 786            if (noAnimation) {
 787                intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
 788            }
 789            runOnUiThread(
 790                    () -> {
 791                        startActivity(intent);
 792                        if (noAnimation) {
 793                            overridePendingTransition(0, 0);
 794                        }
 795                    });
 796        }
 797        return mRedirectInProcess.get();
 798    }
 799
 800    private void showDialogsIfMainIsOverview() {
 801        Pair<Account, Account> incomplete = null;
 802        if (xmppConnectionService != null && (incomplete = xmppConnectionService.onboardingIncomplete()) != null) {
 803            FinishOnboarding.finish(xmppConnectionService, this, incomplete.first, incomplete.second);
 804        }
 805        if (xmppConnectionService == null || xmppConnectionService.isOnboarding()) {
 806            return;
 807        }
 808        final Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
 809        if (fragment instanceof ConversationsOverviewFragment) {
 810            if (ExceptionHelper.checkForCrash(this)) return;
 811            if (offerToSetupDiallerIntegration()) return;
 812            if (offerToDownloadStickers()) return;
 813            if (openBatteryOptimizationDialogIfNeeded()) return;
 814            if (requestNotificationPermissionIfNeeded()) return;
 815            if (askAboutNomedia()) return;
 816            xmppConnectionService.rescanStickers();
 817        }
 818    }
 819
 820    private String getBatteryOptimizationPreferenceKey() {
 821        @SuppressLint("HardwareIds")
 822        String device = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
 823        return "show_battery_optimization" + (device == null ? "" : device);
 824    }
 825
 826    private void setNeverAskForBatteryOptimizationsAgain() {
 827        getPreferences().edit().putBoolean(getBatteryOptimizationPreferenceKey(), false).apply();
 828    }
 829
 830    private boolean openBatteryOptimizationDialogIfNeeded() {
 831        if (isOptimizingBattery()
 832                && getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) {
 833            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
 834            builder.setTitle(R.string.battery_optimizations_enabled);
 835            builder.setMessage(
 836                    getString(
 837                            R.string.battery_optimizations_enabled_dialog,
 838                            getString(R.string.app_name)));
 839            builder.setPositiveButton(
 840                    R.string.next,
 841                    (dialog, which) -> {
 842                        final Intent intent =
 843                                new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
 844                        final Uri uri = Uri.parse("package:" + getPackageName());
 845                        intent.setData(uri);
 846                        try {
 847                            startActivityForResult(intent, REQUEST_BATTERY_OP);
 848                        } catch (final ActivityNotFoundException e) {
 849                            Toast.makeText(
 850                                            this,
 851                                            R.string.device_does_not_support_battery_op,
 852                                            Toast.LENGTH_SHORT)
 853                                    .show();
 854                        }
 855                    });
 856            builder.setOnDismissListener(dialog -> setNeverAskForBatteryOptimizationsAgain());
 857            final AlertDialog dialog = builder.create();
 858            dialog.setCanceledOnTouchOutside(false);
 859            dialog.show();
 860            return true;
 861        }
 862        return false;
 863    }
 864
 865    private boolean requestNotificationPermissionIfNeeded() {
 866        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
 867                && ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
 868                        != PackageManager.PERMISSION_GRANTED) {
 869            requestPermissions(
 870                    new String[] {Manifest.permission.POST_NOTIFICATIONS},
 871                    REQUEST_POST_NOTIFICATION);
 872            return true;
 873        }
 874        return false;
 875    }
 876
 877    private boolean askAboutNomedia() {
 878        if (getPreferences().contains("nomedia")) return false;
 879
 880        AlertDialog.Builder builder = new AlertDialog.Builder(this);
 881        builder.setTitle("Show media in gallery");
 882        builder.setMessage("Would you like to show received and sent media in your system gallery?");
 883        builder.setPositiveButton(R.string.yes, (dialog, which) -> {
 884            getPreferences().edit().putBoolean("nomedia", false).apply();
 885        });
 886        builder.setNegativeButton(R.string.no, (dialog, which) -> {
 887            getPreferences().edit().putBoolean("nomedia", true).apply();
 888        });
 889        final AlertDialog dialog = builder.create();
 890        dialog.setCanceledOnTouchOutside(false);
 891        dialog.show();
 892        return true;
 893    }
 894
 895    private boolean offerToDownloadStickers() {
 896        int offered = getPreferences().getInt("default_stickers_offered", 0);
 897        if (offered > 0) return false;
 898        getPreferences().edit().putInt("default_stickers_offered", 1).apply();
 899
 900        AlertDialog.Builder builder = new AlertDialog.Builder(this);
 901        builder.setTitle("Download Stickers?");
 902        builder.setMessage("Would you like to download some default sticker packs?");
 903        builder.setPositiveButton(R.string.yes, (dialog, which) -> {
 904            if (hasStoragePermission(REQUEST_DOWNLOAD_STICKERS)) {
 905                downloadStickers();
 906            }
 907        });
 908        builder.setNegativeButton(R.string.no, (dialog, which) -> {
 909            showDialogsIfMainIsOverview();
 910        });
 911        final AlertDialog dialog = builder.create();
 912        dialog.setCanceledOnTouchOutside(false);
 913        dialog.show();
 914        return true;
 915    }
 916
 917    private boolean offerToSetupDiallerIntegration() {
 918        if (mRequestCode == DIALLER_INTEGRATION) {
 919            mRequestCode = -1;
 920            return true;
 921        }
 922        if (Build.VERSION.SDK_INT < 23) return false;
 923        if (Build.VERSION.SDK_INT >= 33) {
 924            if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM) && !getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return false;
 925        } else {
 926            if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return false;
 927        }
 928
 929        Set<String> pstnGateways = xmppConnectionService.getAccounts().stream()
 930            .flatMap(a -> a.getGateways("pstn").stream())
 931            .map(a -> a.getJid().asBareJid().toString()).collect(Collectors.toSet());
 932
 933        if (pstnGateways.size() < 1) return false;
 934        Set<String> fromPrefs = getPreferences().getStringSet("pstn_gateways", Set.of("UPGRADE"));
 935        getPreferences().edit().putStringSet("pstn_gateways", pstnGateways).apply();
 936        pstnGateways.removeAll(fromPrefs);
 937        if (pstnGateways.size() < 1) return false;
 938
 939        if (fromPrefs.contains("UPGRADE")) return false;
 940
 941        AlertDialog.Builder builder = new AlertDialog.Builder(this);
 942        builder.setTitle("Dialler Integration");
 943        builder.setMessage("Cheogram Android is able to integrate with your system's dialler app to allow dialling calls via your configured gateway " + String.join(", ", pstnGateways) + ".\n\nEnabling this integration will require granting microphone permission to the app.  Would you like to enable it now?");
 944        builder.setPositiveButton(R.string.yes, (dialog, which) -> {
 945            final String[] permissions;
 946            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
 947                permissions = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.BLUETOOTH_CONNECT};
 948            } else {
 949                permissions = new String[]{Manifest.permission.RECORD_AUDIO};
 950            }
 951            requestPermissions(permissions, REQUEST_MICROPHONE);
 952        });
 953        builder.setNegativeButton(R.string.no, (dialog, which) -> {
 954            showDialogsIfMainIsOverview();
 955        });
 956        final AlertDialog dialog = builder.create();
 957        dialog.setCanceledOnTouchOutside(false);
 958        dialog.show();
 959        return true;
 960    }
 961
 962    private void notifyFragmentOfBackendConnected(@IdRes int id) {
 963        final Fragment fragment = getFragmentManager().findFragmentById(id);
 964        if (fragment instanceof OnBackendConnected callback) {
 965            callback.onBackendConnected();
 966        }
 967    }
 968
 969    private void refreshFragment(@IdRes int id) {
 970        final Fragment fragment = getFragmentManager().findFragmentById(id);
 971        if (fragment instanceof XmppFragment xmppFragment) {
 972            xmppFragment.refresh();
 973            if (refreshForNewCaps) xmppFragment.refreshForNewCaps(newCapsJids);
 974        }
 975    }
 976
 977    private boolean processViewIntent(final Intent intent) {
 978        final String uuid = intent.getStringExtra(EXTRA_CONVERSATION);
 979        final Conversation conversation =
 980                uuid != null ? xmppConnectionService.findConversationByUuidReliable(uuid) : null;
 981        if (conversation == null) {
 982            Log.d(Config.LOGTAG, "unable to view conversation with uuid:" + uuid);
 983            return false;
 984        }
 985        openConversation(conversation, intent.getExtras());
 986        return true;
 987    }
 988
 989    @Override
 990    public void onRequestPermissionsResult(
 991            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
 992        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
 993        UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
 994        if (grantResults.length > 0) {
 995            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 996                switch (requestCode) {
 997                    case REQUEST_OPEN_MESSAGE:
 998                        refreshUiReal();
 999                        ConversationFragment.openPendingMessage(this);
1000                        break;
1001                    case REQUEST_PLAY_PAUSE:
1002                        ConversationFragment.startStopPending(this);
1003                        break;
1004                    case REQUEST_MICROPHONE:
1005                        Intent intent = new Intent();
1006                        intent.setComponent(new ComponentName("com.android.server.telecom",
1007                            "com.android.server.telecom.settings.EnableAccountPreferenceActivity"));
1008                        try {
1009                            startActivityForResult(intent, DIALLER_INTEGRATION);
1010                        } catch (ActivityNotFoundException e) {
1011                            displayToast("Dialler integration not available on your OS");
1012                        }
1013                        break;
1014                    case REQUEST_DOWNLOAD_STICKERS:
1015                        downloadStickers();
1016                        break;
1017                }
1018            } else {
1019                if (requestCode != REQUEST_POST_NOTIFICATION) showDialogsIfMainIsOverview();
1020            }
1021        } else {
1022            if (requestCode != REQUEST_POST_NOTIFICATION) showDialogsIfMainIsOverview();
1023        }
1024    }
1025
1026    private void downloadStickers() {
1027        Intent intent = new Intent(this, DownloadDefaultStickers.class);
1028        intent.putExtra("tor", xmppConnectionService.useTorToConnect());
1029        ContextCompat.startForegroundService(this, intent);
1030        displayToast("Sticker download started");
1031        showDialogsIfMainIsOverview();
1032    }
1033
1034    @Override
1035    public void onActivityResult(int requestCode, int resultCode, final Intent data) {
1036        super.onActivityResult(requestCode, resultCode, data);
1037
1038        if (requestCode == DIALLER_INTEGRATION) {
1039            mRequestCode = requestCode;
1040            try {
1041                startActivity(new Intent(android.telecom.TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS));
1042            } catch (ActivityNotFoundException e) {
1043                displayToast("Dialler integration not available on your OS");
1044            }
1045            return;
1046        }
1047
1048        ActivityResult activityResult = ActivityResult.of(requestCode, resultCode, data);
1049        if (xmppConnectionService != null) {
1050            handleActivityResult(activityResult);
1051        } else {
1052            this.postponedActivityResult.push(activityResult);
1053        }
1054    }
1055
1056    private void handleActivityResult(final ActivityResult activityResult) {
1057        if (activityResult.resultCode == Activity.RESULT_OK) {
1058            handlePositiveActivityResult(activityResult.requestCode, activityResult.data);
1059        } else {
1060            handleNegativeActivityResult(activityResult.requestCode);
1061        }
1062        if (activityResult.requestCode == REQUEST_BATTERY_OP) {
1063            // the result code is always 0 even when battery permission were granted
1064            requestNotificationPermissionIfNeeded();
1065            XmppConnectionService.toggleForegroundService(xmppConnectionService);
1066        }
1067    }
1068
1069    private void handleNegativeActivityResult(int requestCode) {
1070        Conversation conversation = ConversationFragment.getConversationReliable(this);
1071        switch (requestCode) {
1072            case REQUEST_DECRYPT_PGP:
1073                if (conversation == null) {
1074                    break;
1075                }
1076                conversation.getAccount().getPgpDecryptionService().giveUpCurrentDecryption();
1077                break;
1078            case REQUEST_BATTERY_OP:
1079                setNeverAskForBatteryOptimizationsAgain();
1080                break;
1081        }
1082    }
1083
1084    private void handlePositiveActivityResult(int requestCode, final Intent data) {
1085        Conversation conversation = ConversationFragment.getConversationReliable(this);
1086        if (conversation == null) {
1087            Log.d(Config.LOGTAG, "conversation not found");
1088            return;
1089        }
1090        switch (requestCode) {
1091            case REQUEST_DECRYPT_PGP:
1092                conversation.getAccount().getPgpDecryptionService().continueDecryption(data);
1093                break;
1094            case REQUEST_CHOOSE_PGP_ID:
1095                long id = data.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, 0);
1096                if (id != 0) {
1097                    conversation.getAccount().setPgpSignId(id);
1098                    announcePgp(conversation.getAccount(), null, null, onOpenPGPKeyPublished);
1099                } else {
1100                    choosePgpSignId(conversation.getAccount());
1101                }
1102                break;
1103            case REQUEST_ANNOUNCE_PGP:
1104                announcePgp(conversation.getAccount(), conversation, data, onOpenPGPKeyPublished);
1105                break;
1106        }
1107    }
1108
1109    @Override
1110    protected void onCreate(final Bundle savedInstanceState) {
1111        super.onCreate(savedInstanceState);
1112        savedState = savedInstanceState;
1113        ConversationMenuConfigurator.reloadFeatures(this);
1114        OmemoSetting.load(this);
1115        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_conversations);
1116        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
1117        setSupportActionBar(binding.toolbar);
1118        configureActionBar(getSupportActionBar());
1119        this.getFragmentManager().addOnBackStackChangedListener(this::invalidateActionBarTitle);
1120        this.getFragmentManager().addOnBackStackChangedListener(this::showDialogsIfMainIsOverview);
1121        this.initializeFragments();
1122        this.invalidateActionBarTitle();
1123        final Intent intent;
1124        if (savedInstanceState == null) {
1125            intent = getIntent();
1126        } else {
1127            intent = savedInstanceState.getParcelable("intent");
1128        }
1129        if (isViewOrShareIntent(intent)) {
1130            pendingViewIntent.push(intent);
1131            setIntent(createLauncherIntent(this));
1132        }
1133    }
1134
1135    @Override
1136    public boolean onCreateOptionsMenu(Menu menu) {
1137        getMenuInflater().inflate(R.menu.activity_conversations, menu);
1138        final MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code);
1139        final var reportSpamItem = menu.findItem(R.id.action_report_spam);
1140        final var fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
1141        final var overview = fragment instanceof ConversationsOverviewFragment;
1142        if (qrCodeScanMenuItem != null) {
1143            if (isCameraFeatureAvailable() && (xmppConnectionService == null || !xmppConnectionService.isOnboarding())) {
1144                final var visible = getResources().getBoolean(R.bool.show_qr_code_scan) && overview;
1145                qrCodeScanMenuItem.setVisible(visible);
1146            } else {
1147                qrCodeScanMenuItem.setVisible(false);
1148            }
1149        }
1150        reportSpamItem.setVisible(overview && mainFilter == DRAWER_CHAT_REQUESTS);
1151        return super.onCreateOptionsMenu(menu);
1152    }
1153
1154    @Override
1155    public void onConversationSelected(Conversation conversation) {
1156        clearPendingViewIntent();
1157        if (ConversationFragment.getConversation(this) == conversation) {
1158            Log.d(
1159                    Config.LOGTAG,
1160                    "ignore onConversationSelected() because conversation is already open");
1161            return;
1162        }
1163        openConversation(conversation, null);
1164    }
1165
1166    public void clearPendingViewIntent() {
1167        if (pendingViewIntent.clear()) {
1168            Log.e(Config.LOGTAG, "cleared pending view intent");
1169        }
1170    }
1171
1172    private void displayToast(final String msg) {
1173        runOnUiThread(
1174                () -> Toast.makeText(ConversationsActivity.this, msg, Toast.LENGTH_SHORT).show());
1175    }
1176
1177    @Override
1178    public void onAffiliationChangedSuccessful(Jid jid) {}
1179
1180    @Override
1181    public void onAffiliationChangeFailed(Jid jid, int resId) {
1182        displayToast(getString(resId, jid.asBareJid().toString()));
1183    }
1184
1185    private void openConversation(Conversation conversation, Bundle extras) {
1186        final FragmentManager fragmentManager = getFragmentManager();
1187        executePendingTransactions(fragmentManager);
1188        ConversationFragment conversationFragment =
1189                (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment);
1190        final boolean mainNeedsRefresh;
1191        if (conversationFragment == null) {
1192            mainNeedsRefresh = false;
1193            final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
1194            if (mainFragment instanceof ConversationFragment) {
1195                conversationFragment = (ConversationFragment) mainFragment;
1196            } else {
1197                conversationFragment = new ConversationFragment();
1198                FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
1199                fragmentTransaction.replace(R.id.main_fragment, conversationFragment);
1200                fragmentTransaction.addToBackStack(null);
1201                try {
1202                    fragmentTransaction.commit();
1203                } catch (IllegalStateException e) {
1204                    Log.w(Config.LOGTAG, "sate loss while opening conversation", e);
1205                    // allowing state loss is probably fine since view intents et all are already
1206                    // stored and a click can probably be 'ignored'
1207                    return;
1208                }
1209            }
1210        } else {
1211            mainNeedsRefresh = true;
1212        }
1213        conversationFragment.reInit(conversation, extras == null ? new Bundle() : extras);
1214        if (mainNeedsRefresh) {
1215            refreshFragment(R.id.main_fragment);
1216        }
1217        invalidateActionBarTitle();
1218    }
1219
1220    private static void executePendingTransactions(final FragmentManager fragmentManager) {
1221        try {
1222            fragmentManager.executePendingTransactions();
1223        } catch (final Exception e) {
1224            Log.e(Config.LOGTAG, "unable to execute pending fragment transactions");
1225        }
1226    }
1227
1228    public boolean onXmppUriClicked(Uri uri) {
1229        XmppUri xmppUri = new XmppUri(uri);
1230        if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) {
1231            final Conversation conversation =
1232                    xmppConnectionService.findUniqueConversationByJid(xmppUri);
1233            if (conversation != null) {
1234                if (xmppUri.getParameter("password") != null) {
1235                    xmppConnectionService.providePasswordForMuc(conversation, xmppUri.getParameter("password"));
1236                }
1237                if (xmppUri.isAction("command")) {
1238                    startCommand(conversation.getAccount(), xmppUri.getJid(), xmppUri.getParameter("node"));
1239                } else {
1240                    Bundle extras = new Bundle();
1241                    extras.putString(Intent.EXTRA_TEXT, xmppUri.getBody());
1242                    if (xmppUri.isAction("message")) extras.putString(EXTRA_POST_INIT_ACTION, "message");
1243                    openConversation(conversation, extras);
1244                }
1245                return true;
1246            }
1247        }
1248        return false;
1249    }
1250
1251    public boolean onTelUriClicked(Uri uri, Account acct) {
1252        final String tel;
1253        try {
1254            tel = PhoneNumberUtilWrapper.normalize(this, uri.getSchemeSpecificPart());
1255        } catch (final IllegalArgumentException | NumberParseException | NullPointerException e) {
1256            return false;
1257        }
1258
1259        Set<String> gateways = (acct == null ? xmppConnectionService.getAccounts().stream() : List.of(acct).stream()).flatMap(account ->
1260            Stream.concat(
1261                account.getGateways("pstn").stream(),
1262                account.getGateways("sms").stream()
1263            )
1264        ).map(a -> a.getJid().asBareJid().toString()).collect(Collectors.toSet());
1265
1266        for (String gateway : gateways) {
1267            if (onXmppUriClicked(Uri.parse("xmpp:" + tel + "@" + gateway))) return true;
1268        }
1269
1270        if (gateways.size() == 1 && acct != null) {
1271            openConversation(xmppConnectionService.findOrCreateConversation(acct, Jid.ofLocalAndDomain(tel, gateways.iterator().next()), false, true), null);
1272            return true;
1273        }
1274
1275        return false;
1276    }
1277
1278    @Override
1279    public boolean onOptionsItemSelected(MenuItem item) {
1280        if (MenuDoubleTabUtil.shouldIgnoreTap()) {
1281            return false;
1282        }
1283        switch (item.getItemId()) {
1284            case android.R.id.home:
1285                FragmentManager fm = getFragmentManager();
1286                if (android.os.Build.VERSION.SDK_INT >= 26) {
1287                    Fragment f = fm.getFragments().get(fm.getFragments().size() - 1);
1288                    if (f != null && f instanceof ConversationFragment) {
1289                        if (((ConversationFragment) f).onBackPressed()) {
1290                            return true;
1291                        }
1292                    }
1293                }
1294                if (fm.getBackStackEntryCount() > 0) {
1295                    try {
1296                        fm.popBackStack();
1297                    } catch (IllegalStateException e) {
1298                        Log.w(Config.LOGTAG, "Unable to pop back stack after pressing home button");
1299                    }
1300                    return true;
1301                } else {
1302                    if (binding.drawer != null) binding.drawer.getDrawerLayout().openDrawer(binding.drawer);
1303                    return true;
1304                }
1305            case R.id.action_scan_qr_code:
1306                UriHandlerActivity.scan(this);
1307                return true;
1308            case R.id.action_search_all_conversations:
1309                startActivity(new Intent(this, SearchActivity.class));
1310                return true;
1311            case R.id.action_search_this_conversation: {
1312                final Conversation conversation = ConversationFragment.getConversation(this);
1313                if (conversation == null) {
1314                    return true;
1315                }
1316                final Intent intent = new Intent(this, SearchActivity.class);
1317                intent.putExtra(SearchActivity.EXTRA_CONVERSATION_UUID, conversation.getUuid());
1318                startActivity(intent);
1319                return true;
1320            }
1321            case R.id.action_report_spam: {
1322                final var list = new ArrayList<Conversation>();
1323                populateWithOrderedConversations(list, false);
1324                new AlertDialog.Builder(this)
1325                    .setTitle(R.string.report_spam)
1326                    .setMessage("Do you really want to block all these users and report as SPAM?")
1327                    .setPositiveButton(R.string.yes, (dialog, whichButton) -> {
1328                        for (final var conversation : list) {
1329                            final var m = conversation.getLatestMessage();
1330                            xmppConnectionService.sendBlockRequest(conversation, true, m == null ? null : m.getServerMsgId());
1331                        }
1332                    })
1333                    .setNegativeButton(R.string.no, null).show();
1334                return true;
1335            }
1336        }
1337        return super.onOptionsItemSelected(item);
1338    }
1339
1340    @Override
1341    public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
1342        if (keyCode == KeyEvent.KEYCODE_DPAD_UP && keyEvent.isCtrlPressed()) {
1343            final ConversationFragment conversationFragment = ConversationFragment.get(this);
1344            if (conversationFragment != null && conversationFragment.onArrowUpCtrlPressed()) {
1345                return true;
1346            }
1347        }
1348        return super.onKeyDown(keyCode, keyEvent);
1349    }
1350
1351    @Override
1352    public void onSaveInstanceState(Bundle savedInstanceState) {
1353        final Intent pendingIntent = pendingViewIntent.peek();
1354        savedInstanceState.putParcelable("intent", pendingIntent != null ? pendingIntent : getIntent());
1355        savedInstanceState.putLong("mainFilter", mainFilter);
1356        savedInstanceState.putSerializable("selectedTag", selectedTag);
1357        if (binding.drawer != null) savedInstanceState = binding.drawer.saveInstanceState(savedInstanceState);
1358        if (accountHeader != null) savedInstanceState = accountHeader.saveInstanceState(savedInstanceState);
1359        super.onSaveInstanceState(savedInstanceState);
1360    }
1361
1362    @Override
1363    public void onStart() {
1364        super.onStart();
1365        mRedirectInProcess.set(false);
1366    }
1367
1368    @Override
1369    protected void onNewIntent(final Intent intent) {
1370        super.onNewIntent(intent);
1371        if (isViewOrShareIntent(intent)) {
1372            if (xmppConnectionService != null) {
1373                clearPendingViewIntent();
1374                processViewIntent(intent);
1375            } else {
1376                pendingViewIntent.push(intent);
1377            }
1378        }
1379        setIntent(createLauncherIntent(this));
1380    }
1381
1382    @Override
1383    public void onPause() {
1384        this.mActivityPaused = true;
1385        super.onPause();
1386    }
1387
1388    @Override
1389    public void onResume() {
1390        super.onResume();
1391        this.mActivityPaused = false;
1392    }
1393
1394    private void initializeFragments() {
1395        final FragmentManager fragmentManager = getFragmentManager();
1396        FragmentTransaction transaction = fragmentManager.beginTransaction();
1397        final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
1398        final Fragment secondaryFragment =
1399                fragmentManager.findFragmentById(R.id.secondary_fragment);
1400        if (mainFragment != null) {
1401            if (binding.secondaryFragment != null) {
1402                if (mainFragment instanceof ConversationFragment) {
1403                    getFragmentManager().popBackStack();
1404                    transaction.remove(mainFragment);
1405                    transaction.commit();
1406                    fragmentManager.executePendingTransactions();
1407                    transaction = fragmentManager.beginTransaction();
1408                    transaction.replace(R.id.secondary_fragment, mainFragment);
1409                    transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
1410                    transaction.commit();
1411                    return;
1412                }
1413            } else {
1414                if (secondaryFragment instanceof ConversationFragment) {
1415                    transaction.remove(secondaryFragment);
1416                    transaction.commit();
1417                    getFragmentManager().executePendingTransactions();
1418                    transaction = fragmentManager.beginTransaction();
1419                    transaction.replace(R.id.main_fragment, secondaryFragment);
1420                    transaction.addToBackStack(null);
1421                    transaction.commit();
1422                    return;
1423                }
1424            }
1425        } else {
1426            transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
1427        }
1428        if (binding.secondaryFragment != null && secondaryFragment == null) {
1429            transaction.replace(R.id.secondary_fragment, new ConversationFragment());
1430        }
1431        transaction.commit();
1432    }
1433
1434    private void invalidateActionBarTitle() {
1435        final ActionBar actionBar = getSupportActionBar();
1436        if (actionBar == null) {
1437            return;
1438        }
1439        actionBar.setHomeAsUpIndicator(0);
1440        final FragmentManager fragmentManager = getFragmentManager();
1441        final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
1442        if (mainFragment instanceof ConversationFragment conversationFragment) {
1443            final Conversation conversation = conversationFragment.getConversation();
1444            if (conversation != null) {
1445                actionBar.setTitle(conversation.getName());
1446                actionBar.setDisplayHomeAsUpEnabled(!xmppConnectionService.isOnboarding() || !conversation.getJid().equals(Jid.of("cheogram.com")));
1447                ToolbarUtils.setActionBarOnClickListener(
1448                        binding.toolbar,
1449                        (v) -> { if(!xmppConnectionService.isOnboarding()) openConversationDetails(conversation); }
1450                );
1451                return;
1452            }
1453        }
1454        final Fragment secondaryFragment =
1455                fragmentManager.findFragmentById(R.id.secondary_fragment);
1456        if (secondaryFragment instanceof ConversationFragment conversationFragment) {
1457            final Conversation conversation = conversationFragment.getConversation();
1458            if (conversation != null) {
1459                actionBar.setTitle(conversation.getName());
1460            } else {
1461                actionBar.setTitle(R.string.app_name);
1462            }
1463        } else {
1464            actionBar.setTitle(R.string.app_name);
1465        }
1466        actionBar.setDisplayHomeAsUpEnabled(true);
1467        actionBar.setHomeAsUpIndicator(R.drawable.menu_24dp);
1468        ToolbarUtils.resetActionBarOnClickListeners(binding.toolbar);
1469        ToolbarUtils.setActionBarOnClickListener(
1470                binding.toolbar,
1471                (v) -> { if (binding.drawer != null) binding.drawer.getDrawerLayout().openDrawer(binding.drawer); }
1472        );
1473    }
1474
1475    private void openConversationDetails(final Conversation conversation) {
1476        if (conversation.getMode() == Conversational.MODE_MULTI) {
1477            ConferenceDetailsActivity.open(this, conversation);
1478        } else {
1479            final Contact contact = conversation.getContact();
1480            if (contact.isSelf()) {
1481                switchToAccount(conversation.getAccount());
1482            } else {
1483                switchToContactDetails(contact);
1484            }
1485        }
1486    }
1487
1488    @Override
1489    public void onConversationArchived(Conversation conversation) {
1490        if (performRedirectIfNecessary(conversation, false)) {
1491            return;
1492        }
1493        final FragmentManager fragmentManager = getFragmentManager();
1494        final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
1495        if (mainFragment instanceof ConversationFragment) {
1496            try {
1497                fragmentManager.popBackStack();
1498            } catch (final IllegalStateException e) {
1499                Log.w(
1500                        Config.LOGTAG,
1501                        "state loss while popping back state after archiving conversation",
1502                        e);
1503                // this usually means activity is no longer active; meaning on the next open we will
1504                // run through this again
1505            }
1506            return;
1507        }
1508        final Fragment secondaryFragment =
1509                fragmentManager.findFragmentById(R.id.secondary_fragment);
1510        if (secondaryFragment instanceof ConversationFragment) {
1511            if (((ConversationFragment) secondaryFragment).getConversation() == conversation) {
1512                Conversation suggestion =
1513                        ConversationsOverviewFragment.getSuggestion(this, conversation);
1514                if (suggestion != null) {
1515                    openConversation(suggestion, null);
1516                }
1517            }
1518        }
1519    }
1520
1521    @Override
1522    public void onConversationsListItemUpdated() {
1523        Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
1524        if (fragment instanceof ConversationsOverviewFragment) {
1525            ((ConversationsOverviewFragment) fragment).refresh();
1526        }
1527    }
1528
1529    @Override
1530    public void switchToConversation(Conversation conversation) {
1531        Log.d(Config.LOGTAG, "override");
1532        openConversation(conversation, null);
1533    }
1534
1535    @Override
1536    public void onConversationRead(Conversation conversation, String upToUuid) {
1537        if (!mActivityPaused && pendingViewIntent.peek() == null) {
1538            xmppConnectionService.sendReadMarker(conversation, upToUuid);
1539        } else {
1540            Log.d(Config.LOGTAG, "ignoring read callback. mActivityPaused=" + mActivityPaused);
1541        }
1542    }
1543
1544    @Override
1545    public void onAccountUpdate() {
1546        refreshAccounts = true;
1547        this.refreshUi();
1548    }
1549
1550    @Override
1551    public void onConversationUpdate(boolean newCaps) {
1552        if (performRedirectIfNecessary(false)) {
1553            return;
1554        }
1555        refreshForNewCaps = newCaps;
1556        this.refreshUi();
1557    }
1558
1559    @Override
1560    public void onRosterUpdate(final XmppConnectionService.UpdateRosterReason reason, final Contact contact) {
1561        if (reason != XmppConnectionService.UpdateRosterReason.AVATAR) {
1562            refreshForNewCaps = true;
1563            if (contact != null) newCapsJids.add(contact.getJid().asBareJid());
1564        }
1565        this.refreshUi();
1566    }
1567
1568    @Override
1569    public void OnUpdateBlocklist(OnUpdateBlocklist.Status status) {
1570        this.refreshUi();
1571    }
1572
1573    @Override
1574    public void onShowErrorToast(int resId) {
1575        runOnUiThread(() -> Toast.makeText(this, resId, Toast.LENGTH_SHORT).show());
1576    }
1577}