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