ConversationsActivity.java

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