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