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