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