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