XmppActivity.java

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