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