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