XmppActivity.java

   1package eu.siacs.conversations.ui;
   2
   3import android.telephony.TelephonyManager;
   4
   5import android.Manifest;
   6import android.annotation.SuppressLint;
   7import android.app.NotificationManager;
   8import android.app.PendingIntent;
   9import android.content.ActivityNotFoundException;
  10import android.content.ClipData;
  11import android.content.ClipboardManager;
  12import android.content.ComponentName;
  13import android.content.Context;
  14import android.content.ContextWrapper;
  15import android.content.DialogInterface;
  16import android.content.Intent;
  17import android.content.IntentSender.SendIntentException;
  18import android.content.ServiceConnection;
  19import android.content.SharedPreferences;
  20import android.content.pm.PackageManager;
  21import android.content.pm.ResolveInfo;
  22import android.content.res.Configuration;
  23import android.content.res.Resources;
  24import android.graphics.Bitmap;
  25import android.graphics.Point;
  26import android.graphics.drawable.AnimatedImageDrawable;
  27import android.graphics.drawable.BitmapDrawable;
  28import android.graphics.drawable.Drawable;
  29import android.net.ConnectivityManager;
  30import android.net.Uri;
  31import android.os.AsyncTask;
  32import android.os.Build;
  33import android.os.Bundle;
  34import android.os.Handler;
  35import android.os.IBinder;
  36import android.os.PowerManager;
  37import android.os.SystemClock;
  38import android.preference.PreferenceManager;
  39import android.provider.Settings;
  40import android.text.Html;
  41import android.text.InputType;
  42import android.util.DisplayMetrics;
  43import android.util.Log;
  44import android.util.Pair;
  45import android.view.Menu;
  46import android.view.MenuItem;
  47import android.view.View;
  48import android.webkit.ValueCallback;
  49import android.widget.Button;
  50import android.widget.CheckBox;
  51import android.widget.ImageView;
  52import android.widget.Toast;
  53import androidx.annotation.BoolRes;
  54import androidx.annotation.NonNull;
  55import androidx.annotation.RequiresApi;
  56import androidx.annotation.StringRes;
  57import androidx.appcompat.app.AlertDialog;
  58import androidx.appcompat.app.AppCompatDelegate;
  59import androidx.core.content.pm.ShortcutInfoCompat;
  60import androidx.core.content.pm.ShortcutManagerCompat;
  61import androidx.databinding.DataBindingUtil;
  62import com.google.android.material.color.MaterialColors;
  63import com.google.android.material.dialog.MaterialAlertDialogBuilder;
  64import com.google.common.base.Strings;
  65import com.google.common.collect.Collections2;
  66import com.google.common.collect.ImmutableSet;
  67
  68import java.io.IOException;
  69import java.lang.ref.WeakReference;
  70import java.util.ArrayList;
  71import java.util.HashMap;
  72import java.util.List;
  73import java.util.PriorityQueue;
  74import java.util.concurrent.atomic.AtomicReference;
  75import java.util.concurrent.RejectedExecutionException;
  76
  77import eu.siacs.conversations.AppSettings;
  78import eu.siacs.conversations.BuildConfig;
  79import eu.siacs.conversations.Config;
  80import eu.siacs.conversations.R;
  81import eu.siacs.conversations.crypto.PgpEngine;
  82import eu.siacs.conversations.databinding.DialogAddReactionBinding;
  83import eu.siacs.conversations.databinding.DialogQuickeditBinding;
  84import eu.siacs.conversations.entities.Account;
  85import eu.siacs.conversations.entities.Contact;
  86import eu.siacs.conversations.entities.Conversation;
  87import eu.siacs.conversations.entities.Message;
  88import eu.siacs.conversations.entities.Presences;
  89import eu.siacs.conversations.entities.Reaction;
  90import eu.siacs.conversations.services.AvatarService;
  91import eu.siacs.conversations.services.BarcodeProvider;
  92import eu.siacs.conversations.services.NotificationService;
  93import eu.siacs.conversations.services.QuickConversationsService;
  94import eu.siacs.conversations.services.XmppConnectionService;
  95import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
  96import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
  97import eu.siacs.conversations.ui.util.PresenceSelector;
  98import eu.siacs.conversations.ui.util.SettingsUtils;
  99import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
 100import eu.siacs.conversations.utils.AccountUtils;
 101import eu.siacs.conversations.utils.Compatibility;
 102import eu.siacs.conversations.utils.SignupUtils;
 103import eu.siacs.conversations.utils.ThemeHelper;
 104import eu.siacs.conversations.xmpp.Jid;
 105import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 106import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 107import java.io.IOException;
 108import java.lang.ref.WeakReference;
 109import java.util.ArrayList;
 110import java.util.Collection;
 111import java.util.List;
 112import java.util.concurrent.RejectedExecutionException;
 113import java.util.function.Consumer;
 114
 115public abstract class XmppActivity extends ActionBarActivity {
 116
 117    public static final String EXTRA_ACCOUNT = "account";
 118    protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
 119    protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
 120    protected static final int REQUEST_CHOOSE_PGP_ID = 0x0103;
 121    protected static final int REQUEST_BATTERY_OP = 0x49ff;
 122    protected static final int REQUEST_POST_NOTIFICATION = 0x50ff;
 123    public XmppConnectionService xmppConnectionService;
 124    public boolean xmppConnectionServiceBound = false;
 125
 126    protected static final String FRAGMENT_TAG_DIALOG = "dialog";
 127
 128    private boolean isCameraFeatureAvailable = false;
 129
 130    protected int mTheme;
 131    protected HashMap<Integer,Integer> mCustomColors;
 132    protected boolean mUsingEnterKey = false;
 133    protected boolean mUseTor = false;
 134    protected Toast mToast;
 135    public Runnable onOpenPGPKeyPublished =
 136            () ->
 137                    Toast.makeText(
 138                                    XmppActivity.this,
 139                                    R.string.openpgp_has_been_published,
 140                                    Toast.LENGTH_SHORT)
 141                            .show();
 142    protected ConferenceInvite mPendingConferenceInvite = null;
 143    protected PriorityQueue<Pair<Integer, ValueCallback<Uri[]>>> activityCallbacks =
 144        Build.VERSION.SDK_INT >= 24 ? new PriorityQueue<>((x, y) -> y.first.compareTo(x.first)) : new PriorityQueue<>();
 145    protected ServiceConnection mConnection =
 146            new ServiceConnection() {
 147
 148                @Override
 149                public void onServiceConnected(ComponentName className, IBinder service) {
 150                    XmppConnectionBinder binder = (XmppConnectionBinder) service;
 151                    xmppConnectionService = binder.getService();
 152                    xmppConnectionServiceBound = true;
 153                    registerListeners();
 154                    onBackendConnected();
 155                }
 156
 157                @Override
 158                public void onServiceDisconnected(ComponentName arg0) {
 159                    xmppConnectionServiceBound = false;
 160                }
 161            };
 162    private DisplayMetrics metrics;
 163    private long mLastUiRefresh = 0;
 164    private final Handler mRefreshUiHandler = new Handler();
 165    private final Runnable mRefreshUiRunnable =
 166            () -> {
 167                mLastUiRefresh = SystemClock.elapsedRealtime();
 168                refreshUiReal();
 169            };
 170    private final UiCallback<Conversation> adhocCallback =
 171            new UiCallback<Conversation>() {
 172                @Override
 173                public void success(final Conversation conversation) {
 174                    runOnUiThread(
 175                            () -> {
 176                                switchToConversation(conversation);
 177                                hideToast();
 178                            });
 179                }
 180
 181                @Override
 182                public void error(final int errorCode, Conversation object) {
 183                    runOnUiThread(() -> replaceToast(getString(errorCode)));
 184                }
 185
 186                @Override
 187                public void userInputRequired(PendingIntent pi, Conversation object) {}
 188            };
 189
 190    public static boolean cancelPotentialWork(Message message, ImageView imageView) {
 191        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
 192
 193        if (bitmapWorkerTask != null) {
 194            final Message oldMessage = bitmapWorkerTask.message;
 195            if (oldMessage == null || message != oldMessage) {
 196                bitmapWorkerTask.cancel(true);
 197            } else {
 198                return false;
 199            }
 200        }
 201        return true;
 202    }
 203
 204    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
 205        if (imageView != null) {
 206            final Drawable drawable = imageView.getDrawable();
 207            if (drawable instanceof AsyncDrawable) {
 208                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
 209                return asyncDrawable.getBitmapWorkerTask();
 210            }
 211        }
 212        return null;
 213    }
 214
 215    protected void hideToast() {
 216        final var toast = this.mToast;
 217        if (toast == null) {
 218            return;
 219        }
 220        toast.cancel();
 221    }
 222
 223    protected void replaceToast(String msg) {
 224        replaceToast(msg, true);
 225    }
 226
 227    protected void replaceToast(String msg, boolean showlong) {
 228        hideToast();
 229        mToast = Toast.makeText(this, msg, showlong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT);
 230        mToast.show();
 231    }
 232
 233    public final void refreshUi() {
 234        final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh;
 235        if (diff > Config.REFRESH_UI_INTERVAL) {
 236            mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
 237            mRefreshUiHandler.postDelayed(mRefreshUiRunnable, 1);
 238        } else {
 239            final long next = Config.REFRESH_UI_INTERVAL - diff;
 240            mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
 241            mRefreshUiHandler.postDelayed(mRefreshUiRunnable, next);
 242        }
 243    }
 244
 245    protected abstract void refreshUiReal();
 246
 247    @Override
 248    public void onStart() {
 249        super.onStart();
 250        if (!this.mCustomColors.equals(ThemeHelper.applyCustomColors(this))) {
 251            recreate();
 252        }
 253        if (!xmppConnectionServiceBound) {
 254            connectToBackend();
 255        } else {
 256            this.registerListeners();
 257            this.onBackendConnected();
 258        }
 259        this.mUsingEnterKey = usingEnterKey();
 260        this.mUseTor = useTor();
 261    }
 262
 263    public void connectToBackend() {
 264        Intent intent = new Intent(this, XmppConnectionService.class);
 265        intent.setAction("ui");
 266        try {
 267            startService(intent);
 268        } catch (IllegalStateException e) {
 269            Log.w(Config.LOGTAG, "unable to start service from " + getClass().getSimpleName());
 270        }
 271        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
 272    }
 273
 274    @Override
 275    protected void onStop() {
 276        super.onStop();
 277        if (xmppConnectionServiceBound) {
 278            this.unregisterListeners();
 279            unbindService(mConnection);
 280            xmppConnectionServiceBound = false;
 281        }
 282    }
 283
 284    @RequiresApi(api = Build.VERSION_CODES.R)
 285    protected void configureCustomNotification(final ShortcutInfoCompat shortcut) {
 286        final var notificationManager = getSystemService(NotificationManager.class);
 287        final var channel =
 288                notificationManager.getNotificationChannel(
 289                        NotificationService.MESSAGES_NOTIFICATION_CHANNEL, shortcut.getId());
 290        if (channel != null && channel.getConversationId() != null) {
 291            ShortcutManagerCompat.pushDynamicShortcut(this, shortcut);
 292            openNotificationSettings(shortcut);
 293        } else {
 294            NotificationService.createConversationChannel(this, shortcut);
 295            ShortcutManagerCompat.pushDynamicShortcut(this, shortcut);
 296            openNotificationSettings(shortcut);
 297        }
 298    }
 299
 300    @RequiresApi(api = Build.VERSION_CODES.R)
 301    protected void openNotificationSettings(final ShortcutInfoCompat shortcut) {
 302        final var intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
 303        intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
 304        intent.putExtra(
 305                Settings.EXTRA_CHANNEL_ID, NotificationService.MESSAGES_NOTIFICATION_CHANNEL);
 306        intent.putExtra(Settings.EXTRA_CONVERSATION_ID, shortcut.getId());
 307        startActivity(intent);
 308    }
 309
 310    public boolean hasPgp() {
 311        return xmppConnectionService.getPgpEngine() != null;
 312    }
 313
 314    public void showInstallPgpDialog() {
 315        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
 316        builder.setTitle(getString(R.string.openkeychain_required));
 317        builder.setIconAttribute(android.R.attr.alertDialogIcon);
 318        builder.setMessage(
 319                Html.fromHtml(
 320                        getString(
 321                                R.string.openkeychain_required_long,
 322                                getString(R.string.app_name))));
 323        builder.setNegativeButton(getString(R.string.cancel), null);
 324        builder.setNeutralButton(
 325                getString(R.string.restart),
 326                (dialog, which) -> {
 327                    if (xmppConnectionServiceBound) {
 328                        unbindService(mConnection);
 329                        xmppConnectionServiceBound = false;
 330                    }
 331                    stopService(new Intent(XmppActivity.this, XmppConnectionService.class));
 332                    finish();
 333                });
 334        builder.setPositiveButton(
 335                getString(R.string.install),
 336                (dialog, which) -> {
 337                    final Uri uri =
 338                            Uri.parse("market://details?id=org.sufficientlysecure.keychain");
 339                    Intent marketIntent = new Intent(Intent.ACTION_VIEW, uri);
 340                    PackageManager manager = getApplicationContext().getPackageManager();
 341                    final var infos = manager.queryIntentActivities(marketIntent, 0);
 342                    if (infos.isEmpty()) {
 343                        final var website = Uri.parse("http://www.openkeychain.org/");
 344                        final Intent browserIntent = new Intent(Intent.ACTION_VIEW, website);
 345                        try {
 346                            startActivity(browserIntent);
 347                        } catch (final ActivityNotFoundException e) {
 348                            Toast.makeText(
 349                                            this,
 350                                            R.string.application_found_to_open_website,
 351                                            Toast.LENGTH_LONG)
 352                                    .show();
 353                        }
 354                    } else {
 355                        startActivity(marketIntent);
 356                    }
 357                    finish();
 358                });
 359        builder.create().show();
 360    }
 361
 362    public void addReaction(final Message message, Consumer<Collection<String>> callback) {
 363        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
 364        final var layoutInflater = this.getLayoutInflater();
 365        final DialogAddReactionBinding viewBinding =
 366                DataBindingUtil.inflate(layoutInflater, R.layout.dialog_add_reaction, null, false);
 367        builder.setView(viewBinding.getRoot());
 368        final var dialog = builder.create();
 369        for (final String emoji : Reaction.SUGGESTIONS) {
 370            final Button button =
 371                    (Button)
 372                            layoutInflater.inflate(
 373                                    R.layout.item_emoji_button, viewBinding.emojis, false);
 374            viewBinding.emojis.addView(button);
 375            button.setText(emoji);
 376            button.setOnClickListener(
 377                    v -> {
 378                        final var aggregated = message.getAggregatedReactions();
 379                        if (aggregated.ourReactions.contains(emoji)) {
 380                            callback.accept(aggregated.ourReactions);
 381                        } else {
 382                            final ImmutableSet.Builder<String> reactionBuilder =
 383                                    new ImmutableSet.Builder<>();
 384                            reactionBuilder.addAll(aggregated.ourReactions);
 385                            reactionBuilder.add(emoji);
 386                            callback.accept(reactionBuilder.build());
 387                        }
 388                        dialog.dismiss();
 389                    });
 390        }
 391        dialog.show();
 392    }
 393
 394    protected void deleteAccount(final Account account) {
 395        this.deleteAccount(account, null);
 396    }
 397
 398    protected void deleteAccount(final Account account, final Runnable postDelete) {
 399        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
 400        final View dialogView = getLayoutInflater().inflate(R.layout.dialog_delete_account, null);
 401        final CheckBox deleteFromServer = dialogView.findViewById(R.id.delete_from_server);
 402        builder.setView(dialogView);
 403        builder.setTitle(R.string.mgmt_account_delete);
 404        builder.setPositiveButton(getString(R.string.delete), null);
 405        builder.setNegativeButton(getString(R.string.cancel), null);
 406        final AlertDialog dialog = builder.create();
 407        dialog.setOnShowListener(
 408                dialogInterface -> {
 409                    final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
 410                    button.setOnClickListener(
 411                            v -> {
 412                                final boolean unregister = deleteFromServer.isChecked();
 413                                if (unregister) {
 414                                    if (account.isOnlineAndConnected()) {
 415                                        deleteFromServer.setEnabled(false);
 416                                        button.setText(R.string.please_wait);
 417                                        button.setEnabled(false);
 418                                        xmppConnectionService.unregisterAccount(
 419                                                account,
 420                                                result -> {
 421                                                    runOnUiThread(
 422                                                            () -> {
 423                                                                if (result) {
 424                                                                    dialog.dismiss();
 425                                                                    if (postDelete != null) {
 426                                                                        postDelete.run();
 427                                                                    }
 428                                                                    if (xmppConnectionService
 429                                                                                            .getAccounts()
 430                                                                                            .size()
 431                                                                                    == 0
 432                                                                            && Config
 433                                                                                            .MAGIC_CREATE_DOMAIN
 434                                                                                    != null) {
 435                                                                        final Intent intent =
 436                                                                                SignupUtils
 437                                                                                        .getSignUpIntent(
 438                                                                                                this);
 439                                                                        intent.setFlags(
 440                                                                                Intent
 441                                                                                                .FLAG_ACTIVITY_NEW_TASK
 442                                                                                        | Intent
 443                                                                                                .FLAG_ACTIVITY_CLEAR_TASK);
 444                                                                        startActivity(intent);
 445                                                                    }
 446                                                                } else {
 447                                                                    deleteFromServer.setEnabled(
 448                                                                            true);
 449                                                                    button.setText(R.string.delete);
 450                                                                    button.setEnabled(true);
 451                                                                    Toast.makeText(
 452                                                                                    this,
 453                                                                                    R.string
 454                                                                                            .could_not_delete_account_from_server,
 455                                                                                    Toast
 456                                                                                            .LENGTH_LONG)
 457                                                                            .show();
 458                                                                }
 459                                                            });
 460                                                });
 461                                    } else {
 462                                        Toast.makeText(
 463                                                        this,
 464                                                        R.string.not_connected_try_again,
 465                                                        Toast.LENGTH_LONG)
 466                                                .show();
 467                                    }
 468                                } else {
 469                                    xmppConnectionService.deleteAccount(account);
 470                                    dialog.dismiss();
 471                                    if (xmppConnectionService.getAccounts().size() == 0
 472                                            && Config.MAGIC_CREATE_DOMAIN != null) {
 473                                        final Intent intent = SignupUtils.getSignUpIntent(this);
 474                                        intent.setFlags(
 475                                                Intent.FLAG_ACTIVITY_NEW_TASK
 476                                                        | Intent.FLAG_ACTIVITY_CLEAR_TASK);
 477                                        startActivity(intent);
 478                                    } else if (postDelete != null) {
 479                                        postDelete.run();
 480                                    }
 481                                }
 482                            });
 483                });
 484        dialog.show();
 485    }
 486
 487    protected abstract void onBackendConnected();
 488
 489    protected void registerListeners() {
 490        if (this instanceof XmppConnectionService.OnConversationUpdate) {
 491            this.xmppConnectionService.setOnConversationListChangedListener(
 492                    (XmppConnectionService.OnConversationUpdate) this);
 493        }
 494        if (this instanceof XmppConnectionService.OnAccountUpdate) {
 495            this.xmppConnectionService.setOnAccountListChangedListener(
 496                    (XmppConnectionService.OnAccountUpdate) this);
 497        }
 498        if (this instanceof XmppConnectionService.OnCaptchaRequested) {
 499            this.xmppConnectionService.setOnCaptchaRequestedListener(
 500                    (XmppConnectionService.OnCaptchaRequested) this);
 501        }
 502        if (this instanceof XmppConnectionService.OnRosterUpdate) {
 503            this.xmppConnectionService.setOnRosterUpdateListener(
 504                    (XmppConnectionService.OnRosterUpdate) this);
 505        }
 506        if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
 507            this.xmppConnectionService.setOnMucRosterUpdateListener(
 508                    (XmppConnectionService.OnMucRosterUpdate) this);
 509        }
 510        if (this instanceof OnUpdateBlocklist) {
 511            this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this);
 512        }
 513        if (this instanceof XmppConnectionService.OnShowErrorToast) {
 514            this.xmppConnectionService.setOnShowErrorToastListener(
 515                    (XmppConnectionService.OnShowErrorToast) this);
 516        }
 517        if (this instanceof OnKeyStatusUpdated) {
 518            this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this);
 519        }
 520        if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) {
 521            this.xmppConnectionService.setOnRtpConnectionUpdateListener(
 522                    (XmppConnectionService.OnJingleRtpConnectionUpdate) this);
 523        }
 524    }
 525
 526    protected void unregisterListeners() {
 527        if (this instanceof XmppConnectionService.OnConversationUpdate) {
 528            this.xmppConnectionService.removeOnConversationListChangedListener(
 529                    (XmppConnectionService.OnConversationUpdate) this);
 530        }
 531        if (this instanceof XmppConnectionService.OnAccountUpdate) {
 532            this.xmppConnectionService.removeOnAccountListChangedListener(
 533                    (XmppConnectionService.OnAccountUpdate) this);
 534        }
 535        if (this instanceof XmppConnectionService.OnCaptchaRequested) {
 536            this.xmppConnectionService.removeOnCaptchaRequestedListener(
 537                    (XmppConnectionService.OnCaptchaRequested) this);
 538        }
 539        if (this instanceof XmppConnectionService.OnRosterUpdate) {
 540            this.xmppConnectionService.removeOnRosterUpdateListener(
 541                    (XmppConnectionService.OnRosterUpdate) this);
 542        }
 543        if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
 544            this.xmppConnectionService.removeOnMucRosterUpdateListener(
 545                    (XmppConnectionService.OnMucRosterUpdate) this);
 546        }
 547        if (this instanceof OnUpdateBlocklist) {
 548            this.xmppConnectionService.removeOnUpdateBlocklistListener((OnUpdateBlocklist) this);
 549        }
 550        if (this instanceof XmppConnectionService.OnShowErrorToast) {
 551            this.xmppConnectionService.removeOnShowErrorToastListener(
 552                    (XmppConnectionService.OnShowErrorToast) this);
 553        }
 554        if (this instanceof OnKeyStatusUpdated) {
 555            this.xmppConnectionService.removeOnNewKeysAvailableListener((OnKeyStatusUpdated) this);
 556        }
 557        if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) {
 558            this.xmppConnectionService.removeRtpConnectionUpdateListener(
 559                    (XmppConnectionService.OnJingleRtpConnectionUpdate) this);
 560        }
 561    }
 562
 563    @Override
 564    public boolean onOptionsItemSelected(final MenuItem item) {
 565        switch (item.getItemId()) {
 566            case R.id.action_settings:
 567                startActivity(
 568                        new Intent(
 569                                this, eu.siacs.conversations.ui.activity.SettingsActivity.class));
 570                break;
 571            case R.id.action_privacy_policy:
 572                openPrivacyPolicy();
 573                break;
 574            case R.id.action_accounts:
 575                AccountUtils.launchManageAccounts(this);
 576                break;
 577            case R.id.action_account:
 578                AccountUtils.launchManageAccount(this);
 579                break;
 580            case android.R.id.home:
 581                finish();
 582                break;
 583            case R.id.action_show_qr_code:
 584                showQrCode();
 585                break;
 586        }
 587        return super.onOptionsItemSelected(item);
 588    }
 589
 590    private void openPrivacyPolicy() {
 591        if (BuildConfig.PRIVACY_POLICY == null) {
 592            return;
 593        }
 594        final var viewPolicyIntent = new Intent(Intent.ACTION_VIEW);
 595        viewPolicyIntent.setData(Uri.parse(BuildConfig.PRIVACY_POLICY));
 596        try {
 597            startActivity(viewPolicyIntent);
 598        } catch (final ActivityNotFoundException e) {
 599            Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT)
 600                    .show();
 601        }
 602    }
 603
 604    public void selectPresence(
 605            final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
 606        final Contact contact = conversation.getContact();
 607        if (contact.showInRoster() || contact.isSelf()) {
 608            final Presences presences = contact.getPresences();
 609            if (presences.size() == 0) {
 610                if (contact.isSelf()) {
 611                    conversation.setNextCounterpart(null);
 612                    listener.onPresenceSelected();
 613                } else if (!contact.getOption(Contact.Options.TO)
 614                        && !contact.getOption(Contact.Options.ASKING)
 615                        && contact.getAccount().getStatus() == Account.State.ONLINE) {
 616                    showAskForPresenceDialog(contact);
 617                } else if (!contact.getOption(Contact.Options.TO)
 618                        || !contact.getOption(Contact.Options.FROM)) {
 619                    PresenceSelector.warnMutualPresenceSubscription(this, conversation, listener);
 620                } else {
 621                    conversation.setNextCounterpart(null);
 622                    listener.onPresenceSelected();
 623                }
 624            } else if (presences.size() == 1) {
 625                final String presence = presences.toResourceArray()[0];
 626                conversation.setNextCounterpart(
 627                        PresenceSelector.getNextCounterpart(contact, presence));
 628                listener.onPresenceSelected();
 629            } else {
 630                PresenceSelector.showPresenceSelectionDialog(this, conversation, listener);
 631            }
 632        } else {
 633            showAddToRosterDialog(conversation.getContact());
 634        }
 635    }
 636
 637    @SuppressLint("UnsupportedChromeOsCameraSystemFeature")
 638    @Override
 639    protected void onCreate(Bundle savedInstanceState) {
 640        super.onCreate(savedInstanceState);
 641        metrics = getResources().getDisplayMetrics();
 642        this.isCameraFeatureAvailable =
 643                getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
 644        this.mCustomColors = ThemeHelper.applyCustomColors(this);
 645    }
 646
 647    protected boolean isCameraFeatureAvailable() {
 648        return this.isCameraFeatureAvailable;
 649    }
 650
 651    protected boolean isOptimizingBattery() {
 652        final PowerManager pm = getSystemService(PowerManager.class);
 653        return !pm.isIgnoringBatteryOptimizations(getPackageName());
 654    }
 655
 656    protected boolean isAffectedByDataSaver() {
 657        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 658            final ConnectivityManager cm =
 659                    (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
 660            return cm != null
 661                    && cm.isActiveNetworkMetered()
 662                    && Compatibility.getRestrictBackgroundStatus(cm)
 663                            == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
 664        } else {
 665            return false;
 666        }
 667    }
 668
 669    private boolean usingEnterKey() {
 670        return getBooleanPreference("display_enter_key", R.bool.display_enter_key);
 671    }
 672
 673    private boolean useTor() {
 674        return getBooleanPreference("use_tor", R.bool.use_tor);
 675    }
 676
 677    protected SharedPreferences getPreferences() {
 678        return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
 679    }
 680
 681    protected boolean getBooleanPreference(String name, @BoolRes int res) {
 682        return getPreferences().getBoolean(name, getResources().getBoolean(res));
 683    }
 684
 685    public void startCommand(final Account account, final Jid jid, final String node) {
 686        Intent intent = new Intent(this, ConversationsActivity.class);
 687        intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
 688        intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, xmppConnectionService.findOrCreateConversation(account, jid, false, false).getUuid());
 689        intent.putExtra(ConversationsActivity.EXTRA_POST_INIT_ACTION, "command");
 690        intent.putExtra(ConversationsActivity.EXTRA_NODE, node);
 691        intent.putExtra(ConversationsActivity.EXTRA_JID, (CharSequence) jid);
 692        intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
 693        startActivity(intent);
 694    }
 695
 696    public boolean colorCodeAccounts() {
 697        return xmppConnectionService.getAccounts().size() > 1;
 698    }
 699
 700    public void populateWithOrderedConversations(List<Conversation> list) {
 701        xmppConnectionService.populateWithOrderedConversations(list);
 702    }
 703
 704    public void launchStartConversation() {
 705        StartConversationActivity.launch(this);
 706    }
 707
 708    public void switchToConversation(Conversation conversation) {
 709        switchToConversation(conversation, null);
 710    }
 711
 712    public void switchToConversationAndQuote(Conversation conversation, String text) {
 713        switchToConversation(conversation, text, true, null, false, false);
 714    }
 715
 716    public void switchToConversation(Conversation conversation, String text) {
 717        switchToConversation(conversation, text, false, null, false, false);
 718    }
 719
 720    public void switchToConversationDoNotAppend(Conversation conversation, String text) {
 721        switchToConversation(conversation, text, false, null, false, true);
 722    }
 723
 724    public void highlightInMuc(Conversation conversation, String nick) {
 725        switchToConversation(conversation, null, false, nick, false, false);
 726    }
 727
 728    public void privateMsgInMuc(Conversation conversation, String nick) {
 729        switchToConversation(conversation, null, false, nick, true, false);
 730    }
 731
 732    public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend) {
 733        switchToConversation(conversation, text, asQuote, nick, pm, doNotAppend, null);
 734    }
 735
 736    public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend, String postInit) {
 737        switchToConversation(conversation, text, asQuote, nick, pm, doNotAppend, postInit, null);
 738    }
 739
 740    public void switchToConversation(
 741            Conversation conversation,
 742            String text,
 743            boolean asQuote,
 744            String nick,
 745            boolean pm,
 746            boolean doNotAppend,
 747            String postInit,
 748            String thread) {
 749        if (conversation == null) return;
 750        Intent intent = new Intent(this, ConversationsActivity.class);
 751        intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
 752        intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
 753        intent.putExtra(ConversationsActivity.EXTRA_THREAD, thread);
 754        if (text != null) {
 755            intent.putExtra(Intent.EXTRA_TEXT, text);
 756            if (asQuote) {
 757                intent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, true);
 758            }
 759        }
 760        if (nick != null) {
 761            intent.putExtra(ConversationsActivity.EXTRA_NICK, nick);
 762            intent.putExtra(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, pm);
 763        }
 764        if (doNotAppend) {
 765            intent.putExtra(ConversationsActivity.EXTRA_DO_NOT_APPEND, true);
 766        }
 767        intent.putExtra(ConversationsActivity.EXTRA_POST_INIT_ACTION, postInit);
 768        intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
 769        startActivity(intent);
 770        finish();
 771    }
 772
 773    public void switchToContactDetails(Contact contact) {
 774        switchToContactDetails(contact, null);
 775    }
 776
 777    public void switchToContactDetails(Contact contact, String messageFingerprint) {
 778        Intent intent = new Intent(this, ContactDetailsActivity.class);
 779        intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
 780        intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().asBareJid().toEscapedString());
 781        intent.putExtra("contact", contact.getJid().toEscapedString());
 782        intent.putExtra("fingerprint", messageFingerprint);
 783        startActivity(intent);
 784    }
 785
 786    public void switchToAccount(Account account, String fingerprint) {
 787        switchToAccount(account, false, fingerprint);
 788    }
 789
 790    public void switchToAccount(Account account) {
 791        switchToAccount(account, false, null);
 792    }
 793
 794    public void switchToAccount(Account account, boolean init, String fingerprint) {
 795        Intent intent = new Intent(this, EditAccountActivity.class);
 796        intent.putExtra("jid", account.getJid().asBareJid().toEscapedString());
 797        intent.putExtra("init", init);
 798        if (init) {
 799            intent.setFlags(
 800                    Intent.FLAG_ACTIVITY_NEW_TASK
 801                            | Intent.FLAG_ACTIVITY_CLEAR_TASK
 802                            | Intent.FLAG_ACTIVITY_NO_ANIMATION);
 803        }
 804        if (fingerprint != null) {
 805            intent.putExtra("fingerprint", fingerprint);
 806        }
 807        startActivity(intent);
 808        if (init) {
 809            overridePendingTransition(0, 0);
 810        }
 811    }
 812
 813    protected void delegateUriPermissionsToService(Uri uri) {
 814        Intent intent = new Intent(this, XmppConnectionService.class);
 815        intent.setAction(Intent.ACTION_SEND);
 816        intent.setData(uri);
 817        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
 818        try {
 819            startService(intent);
 820        } catch (Exception e) {
 821            Log.e(Config.LOGTAG, "unable to delegate uri permission", e);
 822        }
 823    }
 824
 825    protected void inviteToConversation(Conversation conversation) {
 826        startActivityForResult(
 827                ChooseContactActivity.create(this, conversation), REQUEST_INVITE_TO_CONVERSATION);
 828    }
 829
 830    protected void announcePgp(
 831            final Account account,
 832            final Conversation conversation,
 833            Intent intent,
 834            final Runnable onSuccess) {
 835        if (account.getPgpId() == 0) {
 836            choosePgpSignId(account);
 837        } else {
 838            final String status = Strings.nullToEmpty(account.getPresenceStatusMessage());
 839            xmppConnectionService
 840                    .getPgpEngine()
 841                    .generateSignature(
 842                            intent,
 843                            account,
 844                            status,
 845                            new UiCallback<String>() {
 846
 847                                @Override
 848                                public void userInputRequired(
 849                                        final PendingIntent pi, final String signature) {
 850                                    try {
 851                                        startIntentSenderForResult(
 852                                                pi.getIntentSender(),
 853                                                REQUEST_ANNOUNCE_PGP,
 854                                                null,
 855                                                0,
 856                                                0,
 857                                                0,
 858                                                Compatibility.pgpStartIntentSenderOptions());
 859                                    } catch (final SendIntentException ignored) {
 860                                    }
 861                                }
 862
 863                                @Override
 864                                public void success(String signature) {
 865                                    account.setPgpSignature(signature);
 866                                    xmppConnectionService.databaseBackend.updateAccount(account);
 867                                    xmppConnectionService.sendPresence(account);
 868                                    if (conversation != null) {
 869                                        conversation.setNextEncryption(Message.ENCRYPTION_PGP);
 870                                        xmppConnectionService.updateConversation(conversation);
 871                                        refreshUi();
 872                                    }
 873                                    if (onSuccess != null) {
 874                                        runOnUiThread(onSuccess);
 875                                    }
 876                                }
 877
 878                                @Override
 879                                public void error(int error, String signature) {
 880                                    if (error == 0) {
 881                                        account.setPgpSignId(0);
 882                                        account.unsetPgpSignature();
 883                                        xmppConnectionService.databaseBackend.updateAccount(
 884                                                account);
 885                                        choosePgpSignId(account);
 886                                    } else {
 887                                        displayErrorDialog(error);
 888                                    }
 889                                }
 890                            });
 891        }
 892    }
 893
 894    protected void choosePgpSignId(final Account account) {
 895        xmppConnectionService
 896                .getPgpEngine()
 897                .chooseKey(
 898                        account,
 899                        new UiCallback<>() {
 900                            @Override
 901                            public void success(final Account a) {}
 902
 903                            @Override
 904                            public void error(int errorCode, Account object) {}
 905
 906                            @Override
 907                            public void userInputRequired(PendingIntent pi, Account object) {
 908                                try {
 909                                    startIntentSenderForResult(
 910                                            pi.getIntentSender(),
 911                                            REQUEST_CHOOSE_PGP_ID,
 912                                            null,
 913                                            0,
 914                                            0,
 915                                            0,
 916                                            Compatibility.pgpStartIntentSenderOptions());
 917                                } catch (final SendIntentException ignored) {
 918                                }
 919                            }
 920                        });
 921    }
 922
 923    protected void displayErrorDialog(final int errorCode) {
 924        runOnUiThread(
 925                () -> {
 926                    final MaterialAlertDialogBuilder builder =
 927                            new MaterialAlertDialogBuilder(XmppActivity.this);
 928                    builder.setTitle(getString(R.string.error));
 929                    builder.setMessage(errorCode);
 930                    builder.setNeutralButton(R.string.accept, null);
 931                    builder.create().show();
 932                });
 933    }
 934
 935    protected void showAddToRosterDialog(final Contact contact) {
 936        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
 937        builder.setTitle(contact.getJid().toString());
 938        builder.setMessage(getString(R.string.not_in_roster));
 939        builder.setNegativeButton(getString(R.string.cancel), null);
 940        builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> {
 941            contact.copySystemTagsToGroups();
 942            xmppConnectionService.createContact(contact, true);
 943        });
 944        builder.create().show();
 945    }
 946
 947    private void showAskForPresenceDialog(final Contact contact) {
 948        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
 949        builder.setTitle(contact.getJid().toString());
 950        builder.setMessage(R.string.request_presence_updates);
 951        builder.setNegativeButton(R.string.cancel, null);
 952        builder.setPositiveButton(
 953                R.string.request_now,
 954                (dialog, which) -> {
 955                    if (xmppConnectionServiceBound) {
 956                        xmppConnectionService.sendPresencePacket(
 957                                contact.getAccount(),
 958                                xmppConnectionService
 959                                        .getPresenceGenerator()
 960                                        .requestPresenceUpdatesFrom(contact));
 961                    }
 962                });
 963        builder.create().show();
 964    }
 965
 966    protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback) {
 967        quickEdit(previousValue, callback, hint, false, false);
 968    }
 969
 970    protected void quickEdit(
 971            String previousValue,
 972            @StringRes int hint,
 973            OnValueEdited callback,
 974            boolean permitEmpty) {
 975        quickEdit(previousValue, callback, hint, false, permitEmpty);
 976    }
 977
 978    protected void quickPasswordEdit(String previousValue, OnValueEdited callback) {
 979        quickEdit(previousValue, callback, R.string.password, true, false);
 980    }
 981
 982    protected void quickEdit(final String previousValue, final OnValueEdited callback, final @StringRes int hint, boolean password, boolean permitEmpty) {
 983        quickEdit(previousValue, callback, hint, password, permitEmpty, false);
 984    }
 985
 986    protected void quickEdit(final String previousValue, final OnValueEdited callback, final @StringRes int hint, boolean password, boolean permitEmpty, boolean alwaysCallback) {
 987        quickEdit(previousValue, callback, hint, password, permitEmpty, alwaysCallback, false);
 988    }
 989
 990    @SuppressLint("InflateParams")
 991    protected void quickEdit(final String previousValue,
 992                           final OnValueEdited callback,
 993                           final @StringRes int hint,
 994                           boolean password,
 995                           boolean permitEmpty,
 996                           boolean alwaysCallback,
 997                           boolean startSelected) {
 998        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
 999        final DialogQuickeditBinding binding =
1000                DataBindingUtil.inflate(
1001                        getLayoutInflater(), R.layout.dialog_quickedit, null, false);
1002        if (password) {
1003            binding.inputEditText.setInputType(
1004                    InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
1005        }
1006        builder.setPositiveButton(R.string.accept, null);
1007        if (hint != 0) {
1008            binding.inputLayout.setHint(getString(hint));
1009        }
1010        binding.inputEditText.requestFocus();
1011        if (previousValue != null) {
1012            binding.inputEditText.getText().append(previousValue);
1013        }
1014        builder.setView(binding.getRoot());
1015        builder.setNegativeButton(R.string.cancel, null);
1016        final AlertDialog dialog = builder.create();
1017        dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(binding.inputEditText));
1018        dialog.show();
1019        if (startSelected) {
1020            binding.inputEditText.selectAll();
1021        }
1022        View.OnClickListener clickListener =
1023                v -> {
1024                    String value = binding.inputEditText.getText().toString();
1025                    if ((alwaysCallback || !value.equals(previousValue)) && (!value.trim().isEmpty() || permitEmpty)) {
1026                        String error = callback.onValueEdited(value);
1027                        if (error != null) {
1028                            binding.inputLayout.setError(error);
1029                            return;
1030                        }
1031                    }
1032                    SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
1033                    dialog.dismiss();
1034                };
1035        dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener);
1036        dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
1037                .setOnClickListener(
1038                        (v -> {
1039                            SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
1040                            dialog.dismiss();
1041                        }));
1042        dialog.setCanceledOnTouchOutside(false);
1043        dialog.setOnDismissListener(
1044                dialog1 -> {
1045                    SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
1046                });
1047    }
1048
1049    protected boolean hasStoragePermission(int requestCode) {
1050        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
1051            if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
1052                    != PackageManager.PERMISSION_GRANTED) {
1053                requestPermissions(
1054                        new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
1055                return false;
1056            } else {
1057                return true;
1058            }
1059        } else {
1060            return true;
1061        }
1062    }
1063
1064    public synchronized void startActivityWithCallback(Intent intent, ValueCallback<Uri[]> cb) {
1065        Pair<Integer, ValueCallback<Uri[]>> peek = activityCallbacks.peek();
1066        int index = peek == null ? 1 : peek.first + 1;
1067        activityCallbacks.add(new Pair<>(index, cb));
1068        startActivityForResult(intent, index);
1069    }
1070
1071    protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
1072        super.onActivityResult(requestCode, resultCode, data);
1073        if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
1074            mPendingConferenceInvite = ConferenceInvite.parse(data);
1075            if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
1076                if (mPendingConferenceInvite.execute(this)) {
1077                    mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
1078                    mToast.show();
1079                }
1080                mPendingConferenceInvite = null;
1081            }
1082        } else if (resultCode == RESULT_OK) {
1083            for (Pair<Integer, ValueCallback<Uri[]>> cb : new ArrayList<>(activityCallbacks)) {
1084                if (cb.first == requestCode) {
1085                    activityCallbacks.remove(cb);
1086                    ArrayList<Uri> dataUris = new ArrayList<>();
1087                    if (data.getDataString() != null) {
1088                        dataUris.add(Uri.parse(data.getDataString()));
1089                    } else if (data.getClipData() != null) {
1090                        for (int i = 0; i < data.getClipData().getItemCount(); i++) {
1091                            dataUris.add(data.getClipData().getItemAt(i).getUri());
1092                        }
1093                    }
1094                    cb.second.onReceiveValue(dataUris.toArray(new Uri[0]));
1095                }
1096            }
1097        }
1098    }
1099
1100    public boolean copyTextToClipboard(String text, int labelResId) {
1101        ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
1102        String label = getResources().getString(labelResId);
1103        if (mClipBoardManager != null) {
1104            ClipData mClipData = ClipData.newPlainText(label, text);
1105            mClipBoardManager.setPrimaryClip(mClipData);
1106            return true;
1107        }
1108        return false;
1109    }
1110
1111    protected boolean manuallyChangePresence() {
1112        return getBooleanPreference(
1113                AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
1114    }
1115
1116    protected String getShareableUri() {
1117        return getShareableUri(false);
1118    }
1119
1120    protected String getShareableUri(boolean http) {
1121        return null;
1122    }
1123
1124    protected void shareLink(boolean http) {
1125        String uri = getShareableUri(http);
1126        if (uri == null || uri.isEmpty()) {
1127            return;
1128        }
1129        Intent intent = new Intent(Intent.ACTION_SEND);
1130        intent.setType("text/plain");
1131        intent.putExtra(Intent.EXTRA_TEXT, getShareableUri(http));
1132        try {
1133            startActivity(Intent.createChooser(intent, getText(R.string.share_uri_with)));
1134        } catch (ActivityNotFoundException e) {
1135            Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
1136        }
1137    }
1138
1139    protected void launchOpenKeyChain(long keyId) {
1140        PgpEngine pgp = XmppActivity.this.xmppConnectionService.getPgpEngine();
1141        try {
1142            startIntentSenderForResult(
1143                    pgp.getIntentForKey(keyId).getIntentSender(),
1144                    0,
1145                    null,
1146                    0,
1147                    0,
1148                    0,
1149                    Compatibility.pgpStartIntentSenderOptions());
1150        } catch (final Throwable e) {
1151            Log.d(Config.LOGTAG, "could not launch OpenKeyChain", e);
1152            Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show();
1153        }
1154    }
1155
1156    @Override
1157    protected void onResume() {
1158        super.onResume();
1159        SettingsUtils.applyScreenshotSetting(this);
1160    }
1161
1162    @Override
1163    public void onPause() {
1164        super.onPause();
1165    }
1166
1167    @Override
1168    public boolean onMenuOpened(int id, Menu menu) {
1169        if (id == AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR && menu != null) {
1170            MenuDoubleTabUtil.recordMenuOpen();
1171        }
1172        return super.onMenuOpened(id, menu);
1173    }
1174
1175    protected void showQrCode() {
1176        final var uri = getShareableUri();
1177        if (uri != null) {
1178            showQrCode(uri);
1179            return;
1180        }
1181
1182        final var accounts = xmppConnectionService.getAccounts();
1183        if (accounts.size() < 1) return;
1184
1185        if (accounts.size() == 1) {
1186            showQrCode(accounts.get(0).getShareableUri());
1187            return;
1188        }
1189
1190        final AtomicReference<Account> selectedAccount = new AtomicReference<>(accounts.get(0));
1191        final MaterialAlertDialogBuilder alertDialogBuilder = new MaterialAlertDialogBuilder(this);
1192        alertDialogBuilder.setTitle(R.string.choose_account);
1193        final String[] asStrings = Collections2.transform(accounts, a -> a.getJid().asBareJid().toEscapedString()).toArray(new String[0]);
1194        alertDialogBuilder.setSingleChoiceItems(asStrings, 0, (dialog, which) -> selectedAccount.set(accounts.get(which)));
1195        alertDialogBuilder.setNegativeButton(R.string.cancel, null);
1196        alertDialogBuilder.setPositiveButton(R.string.ok, (dialog, which) -> showQrCode(selectedAccount.get().getShareableUri()));
1197        alertDialogBuilder.create().show();
1198    }
1199
1200    protected void showQrCode(final String uri) {
1201        if (uri == null || uri.isEmpty()) {
1202            return;
1203        }
1204        final Point size = new Point();
1205        getWindowManager().getDefaultDisplay().getSize(size);
1206        final int width = Math.min(size.x, size.y);
1207        final int black;
1208        final int white;
1209        if (Activities.isNightMode(this)) {
1210            black =
1211                    MaterialColors.getColor(
1212                            this,
1213                            com.google.android.material.R.attr.colorSurfaceContainerHighest,
1214                            "No surface color configured");
1215            white =
1216                    MaterialColors.getColor(
1217                            this,
1218                            com.google.android.material.R.attr.colorSurfaceInverse,
1219                            "No inverse surface color configured");
1220        } else {
1221            black =
1222                    MaterialColors.getColor(
1223                            this,
1224                            com.google.android.material.R.attr.colorSurfaceInverse,
1225                            "No inverse surface color configured");
1226            white =
1227                    MaterialColors.getColor(
1228                            this,
1229                            com.google.android.material.R.attr.colorSurfaceContainerHighest,
1230                            "No surface color configured");
1231        }
1232        final var bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width, black, white);
1233        final ImageView view = new ImageView(this);
1234        view.setBackgroundColor(white);
1235        view.setImageBitmap(bitmap);
1236        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1237        builder.setView(view);
1238        builder.create().show();
1239    }
1240
1241    protected Account extractAccount(Intent intent) {
1242        final String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
1243        try {
1244            return jid != null ? xmppConnectionService.findAccountByJid(Jid.ofEscaped(jid)) : null;
1245        } catch (IllegalArgumentException e) {
1246            return null;
1247        }
1248    }
1249
1250    public AvatarService avatarService() {
1251        return xmppConnectionService.getAvatarService();
1252    }
1253
1254    public void loadBitmap(Message message, ImageView imageView) {
1255        Drawable bm;
1256        try {
1257            bm =
1258                    xmppConnectionService
1259                            .getFileBackend()
1260                            .getThumbnail(message, getResources(), (int) (metrics.density * 288), true);
1261        } catch (IOException e) {
1262            bm = null;
1263        }
1264        if (bm != null) {
1265            cancelPotentialWork(message, imageView);
1266            imageView.setImageDrawable(bm);
1267            imageView.setBackgroundColor(0x00000000);
1268            if (Build.VERSION.SDK_INT >= 28 && bm instanceof AnimatedImageDrawable) {
1269                ((AnimatedImageDrawable) bm).start();
1270            }
1271        } else {
1272            if (cancelPotentialWork(message, imageView)) {
1273                final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1274                final BitmapDrawable fallbackThumb = xmppConnectionService.getFileBackend().getFallbackThumbnail(message, (int) (metrics.density * 288), true);
1275                imageView.setBackgroundColor(fallbackThumb == null ? 0xff333333 : 0x00000000);
1276                final AsyncDrawable asyncDrawable = new AsyncDrawable(
1277                        getResources(), fallbackThumb != null ? fallbackThumb.getBitmap() : null, task);
1278                imageView.setImageDrawable(asyncDrawable);
1279                try {
1280                    task.execute(message);
1281                } catch (final RejectedExecutionException ignored) {
1282                    ignored.printStackTrace();
1283                }
1284            }
1285        }
1286    }
1287
1288    protected interface OnValueEdited {
1289        String onValueEdited(String value);
1290    }
1291
1292    public static class ConferenceInvite {
1293        private String uuid;
1294        private final List<Jid> jids = new ArrayList<>();
1295
1296        public static ConferenceInvite parse(Intent data) {
1297            ConferenceInvite invite = new ConferenceInvite();
1298            invite.uuid = data.getStringExtra(ChooseContactActivity.EXTRA_CONVERSATION);
1299            if (invite.uuid == null) {
1300                return null;
1301            }
1302            invite.jids.addAll(ChooseContactActivity.extractJabberIds(data));
1303            return invite;
1304        }
1305
1306        public boolean execute(XmppActivity activity) {
1307            XmppConnectionService service = activity.xmppConnectionService;
1308            Conversation conversation = service.findConversationByUuid(this.uuid);
1309            if (conversation == null) {
1310                return false;
1311            }
1312            if (conversation.getMode() == Conversation.MODE_MULTI) {
1313                for (Jid jid : jids) {
1314                    service.invite(conversation, jid);
1315                }
1316                return false;
1317            } else {
1318                jids.add(conversation.getJid().asBareJid());
1319                return service.createAdhocConference(
1320                        conversation.getAccount(), null, jids, activity.adhocCallback);
1321            }
1322        }
1323    }
1324
1325    static class BitmapWorkerTask extends AsyncTask<Message, Void, Drawable> {
1326        private final WeakReference<ImageView> imageViewReference;
1327        private Message message = null;
1328
1329        private BitmapWorkerTask(ImageView imageView) {
1330            this.imageViewReference = new WeakReference<>(imageView);
1331        }
1332
1333        @Override
1334        protected Drawable doInBackground(Message... params) {
1335            if (isCancelled()) {
1336                return null;
1337            }
1338            final XmppActivity activity = find(imageViewReference);
1339            Drawable d = null;
1340            message = params[0];
1341            try {
1342                if (activity != null && activity.xmppConnectionService != null) {
1343                    d = activity.xmppConnectionService.getFileBackend().getThumbnail(message, imageViewReference.get().getContext().getResources(), (int) (activity.metrics.density * 288), false);
1344                }
1345            } catch (IOException e) { e.printStackTrace(); }
1346            final ImageView imageView = imageViewReference.get();
1347            if (d == null && activity != null && activity.xmppConnectionService != null && imageView != null && imageView.getDrawable() instanceof AsyncDrawable && ((AsyncDrawable) imageView.getDrawable()).getBitmap() == null) {
1348                d = activity.xmppConnectionService.getFileBackend().getFallbackThumbnail(message, (int) (activity.metrics.density * 288), false);
1349            }
1350            return d;
1351        }
1352
1353        @Override
1354        protected void onPostExecute(final Drawable drawable) {
1355            if (!isCancelled()) {
1356                final ImageView imageView = imageViewReference.get();
1357                if (imageView != null) {
1358                    Drawable old = imageView.getDrawable();
1359                    if (old instanceof AsyncDrawable) {
1360                        ((AsyncDrawable) old).clearTask();
1361                    }
1362                    if (drawable != null) {
1363                        imageView.setImageDrawable(drawable);
1364                    }
1365                    imageView.setBackgroundColor(drawable == null ? 0xff333333 : 0x00000000);
1366                    if (Build.VERSION.SDK_INT >= 28 && drawable instanceof AnimatedImageDrawable) {
1367                        ((AnimatedImageDrawable) drawable).start();
1368                    }
1369                }
1370            }
1371        }
1372    }
1373
1374    private static class AsyncDrawable extends BitmapDrawable {
1375        private WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1376
1377        private AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
1378            super(res, bitmap);
1379            bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
1380        }
1381
1382        private synchronized BitmapWorkerTask getBitmapWorkerTask() {
1383            if (bitmapWorkerTaskReference == null) return null;
1384
1385            return bitmapWorkerTaskReference.get();
1386        }
1387
1388        public synchronized void clearTask() {
1389            bitmapWorkerTaskReference = null;
1390        }
1391    }
1392
1393    public static XmppActivity find(@NonNull WeakReference<ImageView> viewWeakReference) {
1394        final View view = viewWeakReference.get();
1395        return view == null ? null : find(view);
1396    }
1397
1398    public static XmppActivity find(@NonNull final View view) {
1399        Context context = view.getContext();
1400        while (context instanceof ContextWrapper) {
1401            if (context instanceof XmppActivity) {
1402                return (XmppActivity) context;
1403            }
1404            context = ((ContextWrapper) context).getBaseContext();
1405        }
1406        return null;
1407    }
1408
1409    public boolean isDark() {
1410        int nightModeFlags = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
1411        return nightModeFlags == Configuration.UI_MODE_NIGHT_YES;
1412    }
1413}