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