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