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 switchToConversationAndQuote(Conversation conversation, String text) {
 797        switchToConversation(conversation, text, true, null, false, false);
 798    }
 799
 800    public void switchToConversation(Conversation conversation, String text) {
 801        switchToConversation(conversation, text, false, null, false, false);
 802    }
 803
 804    public void switchToConversationDoNotAppend(Conversation conversation, String text) {
 805        switchToConversation(conversation, text, false, null, false, true);
 806    }
 807
 808    public void highlightInMuc(Conversation conversation, String nick) {
 809        switchToConversation(conversation, null, false, nick, false, false);
 810    }
 811
 812    public void privateMsgInMuc(Conversation conversation, String nick) {
 813        switchToConversation(conversation, null, false, nick, true, false);
 814    }
 815
 816    public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend) {
 817        switchToConversation(conversation, text, asQuote, nick, pm, doNotAppend, null);
 818    }
 819
 820    public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend, String postInit) {
 821        switchToConversation(conversation, text, asQuote, nick, pm, doNotAppend, postInit, null);
 822    }
 823
 824    public void switchToConversation(
 825            Conversation conversation,
 826            String text,
 827            boolean asQuote,
 828            String nick,
 829            boolean pm,
 830            boolean doNotAppend,
 831            String postInit,
 832            String thread) {
 833        if (conversation == null) return;
 834        Intent intent = new Intent(this, ConversationsActivity.class);
 835        intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
 836        intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
 837        intent.putExtra(ConversationsActivity.EXTRA_THREAD, thread);
 838        if (text != null) {
 839            intent.putExtra(Intent.EXTRA_TEXT, text);
 840            if (asQuote) {
 841                intent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, true);
 842            }
 843        }
 844        if (nick != null) {
 845            intent.putExtra(ConversationsActivity.EXTRA_NICK, nick);
 846            intent.putExtra(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, pm);
 847        }
 848        if (doNotAppend) {
 849            intent.putExtra(ConversationsActivity.EXTRA_DO_NOT_APPEND, true);
 850        }
 851        intent.putExtra(ConversationsActivity.EXTRA_POST_INIT_ACTION, postInit);
 852        intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
 853        startActivity(intent);
 854        finish();
 855    }
 856
 857    public void switchToContactDetails(Contact contact) {
 858        switchToContactDetails(contact, null);
 859    }
 860
 861    public void switchToContactDetails(Contact contact, String messageFingerprint) {
 862        Intent intent = new Intent(this, ContactDetailsActivity.class);
 863        intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
 864        intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().asBareJid().toString());
 865        intent.putExtra("contact", contact.getJid().toString());
 866        intent.putExtra("fingerprint", messageFingerprint);
 867        startActivity(intent);
 868    }
 869
 870    public void switchToAccount(Account account, String fingerprint) {
 871        switchToAccount(account, false, fingerprint);
 872    }
 873
 874    public void switchToAccount(Account account) {
 875        switchToAccount(account, false, null);
 876    }
 877
 878    public void switchToAccount(Account account, boolean init, String fingerprint) {
 879        Intent intent = new Intent(this, EditAccountActivity.class);
 880        intent.putExtra("jid", account.getJid().asBareJid().toString());
 881        intent.putExtra("init", init);
 882        if (init) {
 883            intent.setFlags(
 884                    Intent.FLAG_ACTIVITY_NEW_TASK
 885                            | Intent.FLAG_ACTIVITY_CLEAR_TASK
 886                            | Intent.FLAG_ACTIVITY_NO_ANIMATION);
 887        }
 888        if (fingerprint != null) {
 889            intent.putExtra("fingerprint", fingerprint);
 890        }
 891        startActivity(intent);
 892        if (init) {
 893            overridePendingTransition(0, 0);
 894        }
 895    }
 896
 897    protected void delegateUriPermissionsToService(Uri uri) {
 898        Intent intent = new Intent(this, XmppConnectionService.class);
 899        intent.setAction(Intent.ACTION_SEND);
 900        intent.setData(uri);
 901        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
 902        try {
 903            startService(intent);
 904        } catch (Exception e) {
 905            Log.e(Config.LOGTAG, "unable to delegate uri permission", e);
 906        }
 907    }
 908
 909    protected void inviteToConversation(Conversation conversation) {
 910        startActivityForResult(
 911                ChooseContactActivity.create(this, conversation), REQUEST_INVITE_TO_CONVERSATION);
 912    }
 913
 914    protected void announcePgp(
 915            final Account account,
 916            final Conversation conversation,
 917            Intent intent,
 918            final Runnable onSuccess) {
 919        if (account.getPgpId() == 0) {
 920            choosePgpSignId(account);
 921        } else {
 922            final String status = Strings.nullToEmpty(account.getPresenceStatusMessage());
 923            xmppConnectionService
 924                    .getPgpEngine()
 925                    .generateSignature(
 926                            intent,
 927                            account,
 928                            status,
 929                            new UiCallback<String>() {
 930
 931                                @Override
 932                                public void userInputRequired(
 933                                        final PendingIntent pi, final String signature) {
 934                                    try {
 935                                        startIntentSenderForResult(
 936                                                pi.getIntentSender(),
 937                                                REQUEST_ANNOUNCE_PGP,
 938                                                null,
 939                                                0,
 940                                                0,
 941                                                0,
 942                                                Compatibility.pgpStartIntentSenderOptions());
 943                                    } catch (final SendIntentException ignored) {
 944                                    }
 945                                }
 946
 947                                @Override
 948                                public void success(String signature) {
 949                                    account.setPgpSignature(signature);
 950                                    xmppConnectionService.databaseBackend.updateAccount(account);
 951                                    account.getXmppConnection()
 952                                            .getManager(PresenceManager.class)
 953                                            .available();
 954                                    if (conversation != null) {
 955                                        conversation.setNextEncryption(Message.ENCRYPTION_PGP);
 956                                        xmppConnectionService.updateConversation(conversation);
 957                                        refreshUi();
 958                                    }
 959                                    if (onSuccess != null) {
 960                                        runOnUiThread(onSuccess);
 961                                    }
 962                                }
 963
 964                                @Override
 965                                public void error(int error, String signature) {
 966                                    if (error == 0) {
 967                                        account.setPgpSignId(0);
 968                                        account.unsetPgpSignature();
 969                                        xmppConnectionService.databaseBackend.updateAccount(
 970                                                account);
 971                                        choosePgpSignId(account);
 972                                    } else {
 973                                        displayErrorDialog(error);
 974                                    }
 975                                }
 976                            });
 977        }
 978    }
 979
 980    protected void choosePgpSignId(final Account account) {
 981        xmppConnectionService
 982                .getPgpEngine()
 983                .chooseKey(
 984                        account,
 985                        new UiCallback<>() {
 986                            @Override
 987                            public void success(final Account a) {}
 988
 989                            @Override
 990                            public void error(int errorCode, Account object) {}
 991
 992                            @Override
 993                            public void userInputRequired(PendingIntent pi, Account object) {
 994                                try {
 995                                    startIntentSenderForResult(
 996                                            pi.getIntentSender(),
 997                                            REQUEST_CHOOSE_PGP_ID,
 998                                            null,
 999                                            0,
1000                                            0,
1001                                            0,
1002                                            Compatibility.pgpStartIntentSenderOptions());
1003                                } catch (final SendIntentException ignored) {
1004                                }
1005                            }
1006                        });
1007    }
1008
1009    protected void displayErrorDialog(final int errorCode) {
1010        runOnUiThread(
1011                () -> {
1012                    final MaterialAlertDialogBuilder builder =
1013                            new MaterialAlertDialogBuilder(XmppActivity.this);
1014                    builder.setTitle(getString(R.string.error));
1015                    builder.setMessage(errorCode);
1016                    builder.setNeutralButton(R.string.accept, null);
1017                    builder.create().show();
1018                });
1019    }
1020
1021    protected void showAddToRosterDialog(final Contact contact) {
1022        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1023        builder.setTitle(contact.getJid().toString());
1024        builder.setMessage(getString(R.string.not_in_roster));
1025        builder.setNegativeButton(getString(R.string.cancel), null);
1026        builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> {
1027            contact.copySystemTagsToGroups();
1028            xmppConnectionService.createContact(contact);
1029        });
1030        builder.create().show();
1031    }
1032
1033    private void showAskForPresenceDialog(final Contact contact) {
1034        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1035        builder.setTitle(contact.getJid().toString());
1036        builder.setMessage(R.string.request_presence_updates);
1037        builder.setNegativeButton(R.string.cancel, null);
1038        builder.setPositiveButton(
1039                R.string.request_now,
1040                (dialog, which) -> {
1041                    final var connection = contact.getAccount().getXmppConnection();
1042                    connection
1043                            .getManager(PresenceManager.class)
1044                            .subscribe(contact.getJid().asBareJid());
1045                });
1046        builder.create().show();
1047    }
1048
1049    protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback) {
1050        quickEdit(previousValue, callback, hint, false, false);
1051    }
1052
1053    protected void quickEdit(
1054            String previousValue,
1055            @StringRes int hint,
1056            OnValueEdited callback,
1057            boolean permitEmpty) {
1058        quickEdit(previousValue, callback, hint, false, permitEmpty);
1059    }
1060
1061    protected void quickPasswordEdit(String previousValue, OnValueEdited callback) {
1062        quickEdit(previousValue, callback, R.string.password, true, false);
1063    }
1064
1065    protected void quickEdit(final String previousValue, final OnValueEdited callback, final @StringRes int hint, boolean password, boolean permitEmpty) {
1066        quickEdit(previousValue, callback, hint, password, permitEmpty, false);
1067    }
1068
1069    protected void quickEdit(final String previousValue, final OnValueEdited callback, final @StringRes int hint, boolean password, boolean permitEmpty, boolean alwaysCallback) {
1070        quickEdit(previousValue, callback, hint, password, permitEmpty, alwaysCallback, false);
1071    }
1072
1073    @SuppressLint("InflateParams")
1074    protected void quickEdit(final String previousValue,
1075                           final OnValueEdited callback,
1076                           final @StringRes int hint,
1077                           boolean password,
1078                           boolean permitEmpty,
1079                           boolean alwaysCallback,
1080                           boolean startSelected) {
1081        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1082        final DialogQuickeditBinding binding =
1083                DataBindingUtil.inflate(
1084                        getLayoutInflater(), R.layout.dialog_quickedit, null, false);
1085        if (password) {
1086            binding.inputEditText.setInputType(
1087                    InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
1088        }
1089        builder.setPositiveButton(R.string.accept, null);
1090        if (hint != 0) {
1091            binding.inputLayout.setHint(getString(hint));
1092        }
1093        binding.inputEditText.requestFocus();
1094        if (previousValue != null) {
1095            binding.inputEditText.getText().append(previousValue);
1096        }
1097        builder.setView(binding.getRoot());
1098        builder.setNegativeButton(R.string.cancel, null);
1099        final AlertDialog dialog = builder.create();
1100        dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(binding.inputEditText));
1101        dialog.show();
1102        if (startSelected) {
1103            binding.inputEditText.selectAll();
1104        }
1105        View.OnClickListener clickListener =
1106                v -> {
1107                    String value = binding.inputEditText.getText().toString();
1108                    if ((alwaysCallback || !value.equals(previousValue)) && (!value.trim().isEmpty() || permitEmpty)) {
1109                        String error = callback.onValueEdited(value);
1110                        if (error != null) {
1111                            binding.inputLayout.setError(error);
1112                            return;
1113                        }
1114                    }
1115                    SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
1116                    dialog.dismiss();
1117                };
1118        dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener);
1119        dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
1120                .setOnClickListener(
1121                        (v -> {
1122                            SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
1123                            dialog.dismiss();
1124                        }));
1125        dialog.setCanceledOnTouchOutside(false);
1126        dialog.setOnDismissListener(
1127                dialog1 -> {
1128                    SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
1129                });
1130    }
1131
1132    protected boolean hasStoragePermission(int requestCode) {
1133        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
1134            if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
1135                    != PackageManager.PERMISSION_GRANTED) {
1136                requestPermissions(
1137                        new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
1138                return false;
1139            } else {
1140                return true;
1141            }
1142        } else {
1143            return true;
1144        }
1145    }
1146
1147    public synchronized void startActivityWithCallback(Intent intent, ValueCallback<Uri[]> cb) {
1148        Pair<Integer, ValueCallback<Uri[]>> peek = activityCallbacks.peek();
1149        int index = peek == null ? 1 : peek.first + 1;
1150        activityCallbacks.add(new Pair<>(index, cb));
1151        startActivityForResult(intent, index);
1152    }
1153
1154    protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
1155        super.onActivityResult(requestCode, resultCode, data);
1156        if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
1157            mPendingConferenceInvite = ConferenceInvite.parse(data);
1158            if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
1159                if (mPendingConferenceInvite.execute(this)) {
1160                    mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
1161                    mToast.show();
1162                }
1163                mPendingConferenceInvite = null;
1164            }
1165        } else if (resultCode == RESULT_OK) {
1166            for (Pair<Integer, ValueCallback<Uri[]>> cb : new ArrayList<>(activityCallbacks)) {
1167                if (cb.first == requestCode) {
1168                    activityCallbacks.remove(cb);
1169                    ArrayList<Uri> dataUris = new ArrayList<>();
1170                    if (data.getDataString() != null) {
1171                        dataUris.add(Uri.parse(data.getDataString()));
1172                    } else if (data.getClipData() != null) {
1173                        for (int i = 0; i < data.getClipData().getItemCount(); i++) {
1174                            dataUris.add(data.getClipData().getItemAt(i).getUri());
1175                        }
1176                    }
1177                    cb.second.onReceiveValue(dataUris.toArray(new Uri[0]));
1178                }
1179            }
1180        }
1181    }
1182
1183    public boolean copyTextToClipboard(String text, int labelResId) {
1184        ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
1185        String label = getResources().getString(labelResId);
1186        if (mClipBoardManager != null) {
1187            ClipData mClipData = ClipData.newPlainText(label, text);
1188            mClipBoardManager.setPrimaryClip(mClipData);
1189            return true;
1190        }
1191        return false;
1192    }
1193
1194    protected boolean manuallyChangePresence() {
1195        return getBooleanPreference(
1196                AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
1197    }
1198
1199    protected String getShareableUri() {
1200        return getShareableUri(false);
1201    }
1202
1203    protected String getShareableUri(boolean http) {
1204        return null;
1205    }
1206
1207    protected void shareLink(boolean http) {
1208        String uri = getShareableUri(http);
1209        if (uri == null || uri.isEmpty()) {
1210            return;
1211        }
1212        Intent intent = new Intent(Intent.ACTION_SEND);
1213        intent.setType("text/plain");
1214        intent.putExtra(Intent.EXTRA_TEXT, getShareableUri(http));
1215        try {
1216            startActivity(Intent.createChooser(intent, getText(R.string.share_uri_with)));
1217        } catch (ActivityNotFoundException e) {
1218            Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
1219        }
1220    }
1221
1222    protected void launchOpenKeyChain(long keyId) {
1223        PgpEngine pgp = XmppActivity.this.xmppConnectionService.getPgpEngine();
1224        try {
1225            startIntentSenderForResult(
1226                    pgp.getIntentForKey(keyId).getIntentSender(),
1227                    0,
1228                    null,
1229                    0,
1230                    0,
1231                    0,
1232                    Compatibility.pgpStartIntentSenderOptions());
1233        } catch (final Throwable e) {
1234            Log.d(Config.LOGTAG, "could not launch OpenKeyChain", e);
1235            Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show();
1236        }
1237    }
1238
1239    @Override
1240    protected void onResume() {
1241        super.onResume();
1242        SettingsUtils.applyScreenshotSetting(this);
1243    }
1244
1245    @Override
1246    public void onPause() {
1247        super.onPause();
1248    }
1249
1250    @Override
1251    public boolean onMenuOpened(int id, Menu menu) {
1252        if (id == AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR && menu != null) {
1253            MenuDoubleTabUtil.recordMenuOpen();
1254        }
1255        return super.onMenuOpened(id, menu);
1256    }
1257
1258    protected void showQrCode() {
1259        final var uri = getShareableUri();
1260        if (uri != null) {
1261            showQrCode(uri);
1262            return;
1263        }
1264
1265        final var accounts = xmppConnectionService.getAccounts();
1266        if (accounts.size() < 1) return;
1267
1268        if (accounts.size() == 1) {
1269            showQrCode(accounts.get(0).getShareableUri());
1270            return;
1271        }
1272
1273        final AtomicReference<Account> selectedAccount = new AtomicReference<>(accounts.get(0));
1274        final MaterialAlertDialogBuilder alertDialogBuilder = new MaterialAlertDialogBuilder(this);
1275        alertDialogBuilder.setTitle(R.string.choose_account);
1276        final String[] asStrings = Collections2.transform(accounts, a -> a.getJid().asBareJid().toString()).toArray(new String[0]);
1277        alertDialogBuilder.setSingleChoiceItems(asStrings, 0, (dialog, which) -> selectedAccount.set(accounts.get(which)));
1278        alertDialogBuilder.setNegativeButton(R.string.cancel, null);
1279        alertDialogBuilder.setPositiveButton(R.string.ok, (dialog, which) -> showQrCode(selectedAccount.get().getShareableUri()));
1280        alertDialogBuilder.create().show();
1281    }
1282
1283    protected void showQrCode(final String uri) {
1284        if (uri == null || uri.isEmpty()) {
1285            return;
1286        }
1287        final Point size = new Point();
1288        getWindowManager().getDefaultDisplay().getSize(size);
1289        final int width = Math.min(size.x, size.y);
1290        final int black;
1291        final int white;
1292        if (Activities.isNightMode(this)) {
1293            black =
1294                    MaterialColors.getColor(
1295                            this,
1296                            com.google.android.material.R.attr.colorSurfaceContainerHighest,
1297                            "No surface color configured");
1298            white =
1299                    MaterialColors.getColor(
1300                            this,
1301                            com.google.android.material.R.attr.colorSurfaceInverse,
1302                            "No inverse surface color configured");
1303        } else {
1304            black =
1305                    MaterialColors.getColor(
1306                            this,
1307                            com.google.android.material.R.attr.colorSurfaceInverse,
1308                            "No inverse surface color configured");
1309            white =
1310                    MaterialColors.getColor(
1311                            this,
1312                            com.google.android.material.R.attr.colorSurfaceContainerHighest,
1313                            "No surface color configured");
1314        }
1315        final var bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width, black, white);
1316        final ImageView view = new ImageView(this);
1317        view.setBackgroundColor(white);
1318        view.setImageBitmap(bitmap);
1319        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1320        builder.setView(view);
1321        builder.create().show();
1322    }
1323
1324    protected Account extractAccount(Intent intent) {
1325        final String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
1326        try {
1327            return jid != null ? xmppConnectionService.findAccountByJid(Jid.of(jid)) : null;
1328        } catch (IllegalArgumentException e) {
1329            return null;
1330        }
1331    }
1332
1333    public AvatarService avatarService() {
1334        return xmppConnectionService.getAvatarService();
1335    }
1336
1337    public void loadBitmap(Message message, ImageView imageView) {
1338        Drawable bm;
1339        try {
1340            bm =
1341                    xmppConnectionService
1342                            .getFileBackend()
1343                            .getThumbnail(message, getResources(), (int) (metrics.density * 288), true);
1344        } catch (IOException e) {
1345            bm = null;
1346        }
1347        if (bm != null) {
1348            cancelPotentialWork(message, imageView);
1349            imageView.setImageDrawable(bm);
1350            imageView.setBackgroundColor(0x00000000);
1351            if (Build.VERSION.SDK_INT >= 28 && bm instanceof AnimatedImageDrawable) {
1352                ((AnimatedImageDrawable) bm).start();
1353            }
1354        } else {
1355            if (cancelPotentialWork(message, imageView)) {
1356                final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1357                final BitmapDrawable fallbackThumb = xmppConnectionService.getFileBackend().getFallbackThumbnail(message, (int) (metrics.density * 288), true);
1358                imageView.setBackgroundColor(fallbackThumb == null ? 0xff333333 : 0x00000000);
1359                final AsyncDrawable asyncDrawable = new AsyncDrawable(
1360                        getResources(), fallbackThumb != null ? fallbackThumb.getBitmap() : null, task);
1361                imageView.setImageDrawable(asyncDrawable);
1362                try {
1363                    task.execute(message);
1364                } catch (final RejectedExecutionException ignored) {
1365                    ignored.printStackTrace();
1366                }
1367            }
1368        }
1369    }
1370
1371    protected interface OnValueEdited {
1372        String onValueEdited(String value);
1373    }
1374
1375    public static class ConferenceInvite {
1376        private String uuid;
1377        private final List<Jid> jids = new ArrayList<>();
1378
1379        public static ConferenceInvite parse(Intent data) {
1380            ConferenceInvite invite = new ConferenceInvite();
1381            invite.uuid = data.getStringExtra(ChooseContactActivity.EXTRA_CONVERSATION);
1382            if (invite.uuid == null) {
1383                return null;
1384            }
1385            invite.jids.addAll(ChooseContactActivity.extractJabberIds(data));
1386            return invite;
1387        }
1388
1389        public boolean execute(XmppActivity activity) {
1390            XmppConnectionService service = activity.xmppConnectionService;
1391            Conversation conversation = service.findConversationByUuid(this.uuid);
1392            if (conversation == null) {
1393                return false;
1394            }
1395            if (conversation.getMode() == Conversation.MODE_MULTI) {
1396                for (Jid jid : jids) {
1397                    service.invite(conversation, jid);
1398                }
1399                return false;
1400            } else {
1401                jids.add(conversation.getJid().asBareJid());
1402                return service.createAdhocConference(
1403                        conversation.getAccount(), null, jids, activity.adhocCallback);
1404            }
1405        }
1406    }
1407
1408    static class BitmapWorkerTask extends AsyncTask<Message, Void, Drawable> {
1409        private final WeakReference<ImageView> imageViewReference;
1410        private Message message = null;
1411
1412        private BitmapWorkerTask(ImageView imageView) {
1413            this.imageViewReference = new WeakReference<>(imageView);
1414        }
1415
1416        @Override
1417        protected Drawable doInBackground(Message... params) {
1418            if (isCancelled()) {
1419                return null;
1420            }
1421            final XmppActivity activity = find(imageViewReference);
1422            Drawable d = null;
1423            message = params[0];
1424            try {
1425                if (activity != null && activity.xmppConnectionService != null) {
1426                    d = activity.xmppConnectionService.getFileBackend().getThumbnail(message, imageViewReference.get().getContext().getResources(), (int) (activity.metrics.density * 288), false);
1427                }
1428            } catch (IOException e) { e.printStackTrace(); }
1429            final ImageView imageView = imageViewReference.get();
1430            if (d == null && activity != null && activity.xmppConnectionService != null && imageView != null && imageView.getDrawable() instanceof AsyncDrawable && ((AsyncDrawable) imageView.getDrawable()).getBitmap() == null) {
1431                d = activity.xmppConnectionService.getFileBackend().getFallbackThumbnail(message, (int) (activity.metrics.density * 288), false);
1432            }
1433            return d;
1434        }
1435
1436        @Override
1437        protected void onPostExecute(final Drawable drawable) {
1438            if (!isCancelled()) {
1439                final ImageView imageView = imageViewReference.get();
1440                if (imageView != null) {
1441                    Drawable old = imageView.getDrawable();
1442                    if (old instanceof AsyncDrawable) {
1443                        ((AsyncDrawable) old).clearTask();
1444                    }
1445                    if (drawable != null) {
1446                        imageView.setImageDrawable(drawable);
1447                    }
1448                    imageView.setBackgroundColor(drawable == null ? 0xff333333 : 0x00000000);
1449                    if (Build.VERSION.SDK_INT >= 28 && drawable instanceof AnimatedImageDrawable) {
1450                        ((AnimatedImageDrawable) drawable).start();
1451                    }
1452                }
1453            }
1454        }
1455    }
1456
1457    private static class AsyncDrawable extends BitmapDrawable {
1458        private WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1459
1460        private AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
1461            super(res, bitmap);
1462            bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
1463        }
1464
1465        private synchronized BitmapWorkerTask getBitmapWorkerTask() {
1466            if (bitmapWorkerTaskReference == null) return null;
1467
1468            return bitmapWorkerTaskReference.get();
1469        }
1470
1471        public synchronized void clearTask() {
1472            bitmapWorkerTaskReference = null;
1473        }
1474    }
1475
1476    public static XmppActivity find(@NonNull WeakReference<ImageView> viewWeakReference) {
1477        final View view = viewWeakReference.get();
1478        return view == null ? null : find(view);
1479    }
1480
1481    public static XmppActivity find(@NonNull final View view) {
1482        Context context = view.getContext();
1483        while (context instanceof ContextWrapper) {
1484            if (context instanceof XmppActivity) {
1485                return (XmppActivity) context;
1486            }
1487            context = ((ContextWrapper) context).getBaseContext();
1488        }
1489        return null;
1490    }
1491
1492    public boolean isDark() {
1493        int nightModeFlags = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
1494        return nightModeFlags == Configuration.UI_MODE_NIGHT_YES;
1495    }
1496}