ConversationsActivity.java

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