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