XmppActivity.java

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