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