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