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