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