ConversationsActivity.java

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