XmppActivity.java

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