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