XmppActivity.java

   1package eu.siacs.conversations.ui;
   2
   3import android.Manifest;
   4import android.annotation.SuppressLint;
   5import android.annotation.TargetApi;
   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.pm.ResolveInfo;
  20import android.content.res.Resources;
  21import android.content.res.TypedArray;
  22import android.graphics.Bitmap;
  23import android.graphics.Color;
  24import android.graphics.Point;
  25import android.graphics.drawable.AnimatedImageDrawable;
  26import android.graphics.drawable.BitmapDrawable;
  27import android.graphics.drawable.Drawable;
  28import android.net.ConnectivityManager;
  29import android.net.Uri;
  30import android.os.AsyncTask;
  31import android.os.Build;
  32import android.os.Bundle;
  33import android.os.Handler;
  34import android.os.IBinder;
  35import android.os.PowerManager;
  36import android.os.SystemClock;
  37import android.preference.PreferenceManager;
  38import android.text.Html;
  39import android.text.InputType;
  40import android.util.DisplayMetrics;
  41import android.util.Log;
  42import android.view.Menu;
  43import android.view.MenuItem;
  44import android.view.View;
  45import android.widget.ImageView;
  46import android.widget.Toast;
  47
  48import androidx.annotation.BoolRes;
  49import androidx.annotation.NonNull;
  50import androidx.annotation.StringRes;
  51import androidx.appcompat.app.AlertDialog;
  52import androidx.appcompat.app.AlertDialog.Builder;
  53import androidx.appcompat.app.AppCompatDelegate;
  54import androidx.databinding.DataBindingUtil;
  55
  56import com.google.common.base.Strings;
  57
  58import java.io.IOException;
  59import java.lang.ref.WeakReference;
  60import java.util.ArrayList;
  61import java.util.List;
  62import java.util.concurrent.RejectedExecutionException;
  63
  64import eu.siacs.conversations.Config;
  65import eu.siacs.conversations.R;
  66import eu.siacs.conversations.crypto.PgpEngine;
  67import eu.siacs.conversations.databinding.DialogQuickeditBinding;
  68import eu.siacs.conversations.entities.Account;
  69import eu.siacs.conversations.entities.Contact;
  70import eu.siacs.conversations.entities.Conversation;
  71import eu.siacs.conversations.entities.Message;
  72import eu.siacs.conversations.entities.Presences;
  73import eu.siacs.conversations.services.AvatarService;
  74import eu.siacs.conversations.services.BarcodeProvider;
  75import eu.siacs.conversations.services.EmojiInitializationService;
  76import eu.siacs.conversations.services.QuickConversationsService;
  77import eu.siacs.conversations.services.XmppConnectionService;
  78import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
  79import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
  80import eu.siacs.conversations.ui.util.PresenceSelector;
  81import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
  82import eu.siacs.conversations.utils.AccountUtils;
  83import eu.siacs.conversations.utils.ExceptionHelper;
  84import eu.siacs.conversations.ui.util.SettingsUtils;
  85import eu.siacs.conversations.utils.ThemeHelper;
  86import eu.siacs.conversations.xmpp.Jid;
  87import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
  88import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
  89
  90public abstract class XmppActivity extends ActionBarActivity {
  91
  92    public static final String EXTRA_ACCOUNT = "account";
  93    protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
  94    protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
  95    protected static final int REQUEST_CHOOSE_PGP_ID = 0x0103;
  96    protected static final int REQUEST_BATTERY_OP = 0x49ff;
  97    public XmppConnectionService xmppConnectionService;
  98    public boolean xmppConnectionServiceBound = false;
  99
 100    protected static final String FRAGMENT_TAG_DIALOG = "dialog";
 101
 102    private boolean isCameraFeatureAvailable = false;
 103
 104    protected int mTheme;
 105    protected boolean mUsingEnterKey = false;
 106    protected boolean mUseTor = false;
 107    protected Toast mToast;
 108    public Runnable onOpenPGPKeyPublished = () -> Toast.makeText(XmppActivity.this, R.string.openpgp_has_been_published, Toast.LENGTH_SHORT).show();
 109    protected ConferenceInvite mPendingConferenceInvite = null;
 110    protected ServiceConnection mConnection = new ServiceConnection() {
 111
 112        @Override
 113        public void onServiceConnected(ComponentName className, IBinder service) {
 114            XmppConnectionBinder binder = (XmppConnectionBinder) service;
 115            xmppConnectionService = binder.getService();
 116            xmppConnectionServiceBound = true;
 117            registerListeners();
 118            onBackendConnected();
 119        }
 120
 121        @Override
 122        public void onServiceDisconnected(ComponentName arg0) {
 123            xmppConnectionServiceBound = false;
 124        }
 125    };
 126    private DisplayMetrics metrics;
 127    private long mLastUiRefresh = 0;
 128    private final Handler mRefreshUiHandler = new Handler();
 129    private final Runnable mRefreshUiRunnable = () -> {
 130        mLastUiRefresh = SystemClock.elapsedRealtime();
 131        refreshUiReal();
 132    };
 133    private final UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
 134        @Override
 135        public void success(final Conversation conversation) {
 136            runOnUiThread(() -> {
 137                switchToConversation(conversation);
 138                hideToast();
 139            });
 140        }
 141
 142        @Override
 143        public void error(final int errorCode, Conversation object) {
 144            runOnUiThread(() -> replaceToast(getString(errorCode)));
 145        }
 146
 147        @Override
 148        public void userInputRequired(PendingIntent pi, Conversation object) {
 149
 150        }
 151    };
 152    public boolean mSkipBackgroundBinding = false;
 153
 154    public static boolean cancelPotentialWork(Message message, ImageView imageView) {
 155        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
 156
 157        if (bitmapWorkerTask != null) {
 158            final Message oldMessage = bitmapWorkerTask.message;
 159            if (oldMessage == null || message != oldMessage) {
 160                bitmapWorkerTask.cancel(true);
 161            } else {
 162                return false;
 163            }
 164        }
 165        return true;
 166    }
 167
 168    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
 169        if (imageView != null) {
 170            final Drawable drawable = imageView.getDrawable();
 171            if (drawable instanceof AsyncDrawable) {
 172                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
 173                return asyncDrawable.getBitmapWorkerTask();
 174            }
 175        }
 176        return null;
 177    }
 178
 179    protected void hideToast() {
 180        if (mToast != null) {
 181            mToast.cancel();
 182        }
 183    }
 184
 185    protected void replaceToast(String msg) {
 186        replaceToast(msg, true);
 187    }
 188
 189    protected void replaceToast(String msg, boolean showlong) {
 190        hideToast();
 191        mToast = Toast.makeText(this, msg, showlong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT);
 192        mToast.show();
 193    }
 194
 195    protected final void refreshUi() {
 196        final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh;
 197        if (diff > Config.REFRESH_UI_INTERVAL) {
 198            mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
 199            runOnUiThread(mRefreshUiRunnable);
 200        } else {
 201            final long next = Config.REFRESH_UI_INTERVAL - diff;
 202            mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
 203            mRefreshUiHandler.postDelayed(mRefreshUiRunnable, next);
 204        }
 205    }
 206
 207    abstract protected void refreshUiReal();
 208
 209    @Override
 210    protected void onStart() {
 211        super.onStart();
 212        if (!xmppConnectionServiceBound) {
 213            if (this.mSkipBackgroundBinding) {
 214                Log.d(Config.LOGTAG, "skipping background binding");
 215            } else {
 216                connectToBackend();
 217            }
 218        } else {
 219            this.registerListeners();
 220            this.onBackendConnected();
 221        }
 222        this.mUsingEnterKey = usingEnterKey();
 223        this.mUseTor = useTor();
 224    }
 225
 226    public void connectToBackend() {
 227        Intent intent = new Intent(this, XmppConnectionService.class);
 228        intent.setAction("ui");
 229        try {
 230            startService(intent);
 231        } catch (IllegalStateException e) {
 232            Log.w(Config.LOGTAG, "unable to start service from " + getClass().getSimpleName());
 233        }
 234        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
 235    }
 236
 237    @Override
 238    protected void onStop() {
 239        super.onStop();
 240        if (xmppConnectionServiceBound) {
 241            this.unregisterListeners();
 242            unbindService(mConnection);
 243            xmppConnectionServiceBound = false;
 244        }
 245    }
 246
 247
 248    public boolean hasPgp() {
 249        return xmppConnectionService.getPgpEngine() != null;
 250    }
 251
 252    public void showInstallPgpDialog() {
 253        Builder builder = new AlertDialog.Builder(this);
 254        builder.setTitle(getString(R.string.openkeychain_required));
 255        builder.setIconAttribute(android.R.attr.alertDialogIcon);
 256        builder.setMessage(Html.fromHtml(getString(R.string.openkeychain_required_long, getString(R.string.app_name))));
 257        builder.setNegativeButton(getString(R.string.cancel), null);
 258        builder.setNeutralButton(getString(R.string.restart),
 259                (dialog, which) -> {
 260                    if (xmppConnectionServiceBound) {
 261                        unbindService(mConnection);
 262                        xmppConnectionServiceBound = false;
 263                    }
 264                    stopService(new Intent(XmppActivity.this,
 265                            XmppConnectionService.class));
 266                    finish();
 267                });
 268        builder.setPositiveButton(getString(R.string.install),
 269                (dialog, which) -> {
 270                    Uri uri = Uri
 271                            .parse("market://details?id=org.sufficientlysecure.keychain");
 272                    Intent marketIntent = new Intent(Intent.ACTION_VIEW,
 273                            uri);
 274                    PackageManager manager = getApplicationContext()
 275                            .getPackageManager();
 276                    List<ResolveInfo> infos = manager
 277                            .queryIntentActivities(marketIntent, 0);
 278                    if (infos.size() > 0) {
 279                        startActivity(marketIntent);
 280                    } else {
 281                        uri = Uri.parse("http://www.openkeychain.org/");
 282                        Intent browserIntent = new Intent(
 283                                Intent.ACTION_VIEW, uri);
 284                        startActivity(browserIntent);
 285                    }
 286                    finish();
 287                });
 288        builder.create().show();
 289    }
 290
 291    abstract void onBackendConnected();
 292
 293    protected void registerListeners() {
 294        if (this instanceof XmppConnectionService.OnConversationUpdate) {
 295            this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this);
 296        }
 297        if (this instanceof XmppConnectionService.OnAccountUpdate) {
 298            this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
 299        }
 300        if (this instanceof XmppConnectionService.OnCaptchaRequested) {
 301            this.xmppConnectionService.setOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this);
 302        }
 303        if (this instanceof XmppConnectionService.OnRosterUpdate) {
 304            this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
 305        }
 306        if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
 307            this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this);
 308        }
 309        if (this instanceof OnUpdateBlocklist) {
 310            this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this);
 311        }
 312        if (this instanceof XmppConnectionService.OnShowErrorToast) {
 313            this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
 314        }
 315        if (this instanceof OnKeyStatusUpdated) {
 316            this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this);
 317        }
 318        if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) {
 319            this.xmppConnectionService.setOnRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this);
 320        }
 321    }
 322
 323    protected void unregisterListeners() {
 324        if (this instanceof XmppConnectionService.OnConversationUpdate) {
 325            this.xmppConnectionService.removeOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this);
 326        }
 327        if (this instanceof XmppConnectionService.OnAccountUpdate) {
 328            this.xmppConnectionService.removeOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
 329        }
 330        if (this instanceof XmppConnectionService.OnCaptchaRequested) {
 331            this.xmppConnectionService.removeOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this);
 332        }
 333        if (this instanceof XmppConnectionService.OnRosterUpdate) {
 334            this.xmppConnectionService.removeOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
 335        }
 336        if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
 337            this.xmppConnectionService.removeOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this);
 338        }
 339        if (this instanceof OnUpdateBlocklist) {
 340            this.xmppConnectionService.removeOnUpdateBlocklistListener((OnUpdateBlocklist) this);
 341        }
 342        if (this instanceof XmppConnectionService.OnShowErrorToast) {
 343            this.xmppConnectionService.removeOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
 344        }
 345        if (this instanceof OnKeyStatusUpdated) {
 346            this.xmppConnectionService.removeOnNewKeysAvailableListener((OnKeyStatusUpdated) this);
 347        }
 348        if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) {
 349            this.xmppConnectionService.removeRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this);
 350        }
 351    }
 352
 353    @Override
 354    public boolean onOptionsItemSelected(final MenuItem item) {
 355        switch (item.getItemId()) {
 356            case R.id.action_settings:
 357                startActivity(new Intent(this, SettingsActivity.class));
 358                break;
 359            case R.id.action_accounts:
 360                AccountUtils.launchManageAccounts(this);
 361                break;
 362            case R.id.action_account:
 363                AccountUtils.launchManageAccount(this);
 364                break;
 365            case android.R.id.home:
 366                finish();
 367                break;
 368            case R.id.action_show_qr_code:
 369                showQrCode();
 370                break;
 371        }
 372        return super.onOptionsItemSelected(item);
 373    }
 374
 375    public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
 376        final Contact contact = conversation.getContact();
 377        if (contact.showInRoster() || contact.isSelf()) {
 378            final Presences presences = contact.getPresences();
 379            if (presences.size() == 0) {
 380                if (contact.isSelf()) {
 381                    conversation.setNextCounterpart(null);
 382                    listener.onPresenceSelected();
 383                } else if (!contact.getOption(Contact.Options.TO)
 384                        && !contact.getOption(Contact.Options.ASKING)
 385                        && contact.getAccount().getStatus() == Account.State.ONLINE) {
 386                    showAskForPresenceDialog(contact);
 387                } else if (!contact.getOption(Contact.Options.TO)
 388                        || !contact.getOption(Contact.Options.FROM)) {
 389                    PresenceSelector.warnMutualPresenceSubscription(this, conversation, listener);
 390                } else {
 391                    conversation.setNextCounterpart(null);
 392                    listener.onPresenceSelected();
 393                }
 394            } else if (presences.size() == 1) {
 395                final String presence = presences.toResourceArray()[0];
 396                conversation.setNextCounterpart(PresenceSelector.getNextCounterpart(contact, presence));
 397                listener.onPresenceSelected();
 398            } else {
 399                PresenceSelector.showPresenceSelectionDialog(this, conversation, listener);
 400            }
 401        } else {
 402            showAddToRosterDialog(conversation.getContact());
 403        }
 404    }
 405
 406    @SuppressLint("UnsupportedChromeOsCameraSystemFeature")
 407    @Override
 408    protected void onCreate(Bundle savedInstanceState) {
 409        super.onCreate(savedInstanceState);
 410        metrics = getResources().getDisplayMetrics();
 411        ExceptionHelper.init(getApplicationContext());
 412        EmojiInitializationService.execute(this);
 413        this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
 414        this.mTheme = findTheme();
 415        setTheme(this.mTheme);
 416    }
 417
 418    protected boolean isCameraFeatureAvailable() {
 419        return this.isCameraFeatureAvailable;
 420    }
 421
 422    public boolean isDarkTheme() {
 423        return ThemeHelper.isDark(mTheme);
 424    }
 425
 426    public int getThemeResource(int r_attr_name, int r_drawable_def) {
 427        int[] attrs = {r_attr_name};
 428        TypedArray ta = this.getTheme().obtainStyledAttributes(attrs);
 429
 430        int res = ta.getResourceId(0, r_drawable_def);
 431        ta.recycle();
 432
 433        return res;
 434    }
 435
 436    protected boolean isOptimizingBattery() {
 437        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 438            final PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
 439            return pm != null
 440                    && !pm.isIgnoringBatteryOptimizations(getPackageName());
 441        } else {
 442            return false;
 443        }
 444    }
 445
 446    protected boolean isAffectedByDataSaver() {
 447        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 448            final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
 449            return cm != null
 450                    && cm.isActiveNetworkMetered()
 451                    && cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
 452        } else {
 453            return false;
 454        }
 455    }
 456
 457    private boolean usingEnterKey() {
 458        return getBooleanPreference("display_enter_key", R.bool.display_enter_key);
 459    }
 460
 461    private boolean useTor() {
 462        return QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor);
 463    }
 464
 465    protected SharedPreferences getPreferences() {
 466        return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
 467    }
 468
 469    protected boolean getBooleanPreference(String name, @BoolRes int res) {
 470        return getPreferences().getBoolean(name, getResources().getBoolean(res));
 471    }
 472
 473    public void switchToConversation(Conversation conversation) {
 474        switchToConversation(conversation, null);
 475    }
 476
 477    public void switchToConversationAndQuote(Conversation conversation, String text) {
 478        switchToConversation(conversation, text, true, null, false, false);
 479    }
 480
 481    public void switchToConversation(Conversation conversation, String text) {
 482        switchToConversation(conversation, text, false, null, false, false);
 483    }
 484
 485    public void switchToConversationDoNotAppend(Conversation conversation, String text) {
 486        switchToConversation(conversation, text, false, null, false, true);
 487    }
 488
 489    public void highlightInMuc(Conversation conversation, String nick) {
 490        switchToConversation(conversation, null, false, nick, false, false);
 491    }
 492
 493    public void privateMsgInMuc(Conversation conversation, String nick) {
 494        switchToConversation(conversation, null, false, nick, true, false);
 495    }
 496
 497    private void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend) {
 498        Intent intent = new Intent(this, ConversationsActivity.class);
 499        intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
 500        intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
 501        if (text != null) {
 502            intent.putExtra(Intent.EXTRA_TEXT, text);
 503            if (asQuote) {
 504                intent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, true);
 505            }
 506        }
 507        if (nick != null) {
 508            intent.putExtra(ConversationsActivity.EXTRA_NICK, nick);
 509            intent.putExtra(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, pm);
 510        }
 511        if (doNotAppend) {
 512            intent.putExtra(ConversationsActivity.EXTRA_DO_NOT_APPEND, true);
 513        }
 514        intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
 515        startActivity(intent);
 516        finish();
 517    }
 518
 519    public void switchToContactDetails(Contact contact) {
 520        switchToContactDetails(contact, null);
 521    }
 522
 523    public void switchToContactDetails(Contact contact, String messageFingerprint) {
 524        Intent intent = new Intent(this, ContactDetailsActivity.class);
 525        intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
 526        intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().asBareJid().toEscapedString());
 527        intent.putExtra("contact", contact.getJid().toEscapedString());
 528        intent.putExtra("fingerprint", messageFingerprint);
 529        startActivity(intent);
 530    }
 531
 532    public void switchToAccount(Account account, String fingerprint) {
 533        switchToAccount(account, false, fingerprint);
 534    }
 535
 536    public void switchToAccount(Account account) {
 537        switchToAccount(account, false, null);
 538    }
 539
 540    public void switchToAccount(Account account, boolean init, String fingerprint) {
 541        Intent intent = new Intent(this, EditAccountActivity.class);
 542        intent.putExtra("jid", account.getJid().asBareJid().toEscapedString());
 543        intent.putExtra("init", init);
 544        if (init) {
 545            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION);
 546        }
 547        if (fingerprint != null) {
 548            intent.putExtra("fingerprint", fingerprint);
 549        }
 550        startActivity(intent);
 551        if (init) {
 552            overridePendingTransition(0, 0);
 553        }
 554    }
 555
 556    protected void delegateUriPermissionsToService(Uri uri) {
 557        Intent intent = new Intent(this, XmppConnectionService.class);
 558        intent.setAction(Intent.ACTION_SEND);
 559        intent.setData(uri);
 560        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
 561        try {
 562            startService(intent);
 563        } catch (Exception e) {
 564            Log.e(Config.LOGTAG, "unable to delegate uri permission", e);
 565        }
 566    }
 567
 568    protected void inviteToConversation(Conversation conversation) {
 569        startActivityForResult(ChooseContactActivity.create(this, conversation), REQUEST_INVITE_TO_CONVERSATION);
 570    }
 571
 572    protected void announcePgp(final Account account, final Conversation conversation, Intent intent, final Runnable onSuccess) {
 573        if (account.getPgpId() == 0) {
 574            choosePgpSignId(account);
 575        } else {
 576            final String status = Strings.nullToEmpty(account.getPresenceStatusMessage());
 577            xmppConnectionService.getPgpEngine().generateSignature(intent, account, status, new UiCallback<String>() {
 578
 579                @Override
 580                public void userInputRequired(PendingIntent pi, String signature) {
 581                    try {
 582                        startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
 583                    } catch (final SendIntentException ignored) {
 584                    }
 585                }
 586
 587                @Override
 588                public void success(String signature) {
 589                    account.setPgpSignature(signature);
 590                    xmppConnectionService.databaseBackend.updateAccount(account);
 591                    xmppConnectionService.sendPresence(account);
 592                    if (conversation != null) {
 593                        conversation.setNextEncryption(Message.ENCRYPTION_PGP);
 594                        xmppConnectionService.updateConversation(conversation);
 595                        refreshUi();
 596                    }
 597                    if (onSuccess != null) {
 598                        runOnUiThread(onSuccess);
 599                    }
 600                }
 601
 602                @Override
 603                public void error(int error, String signature) {
 604                    if (error == 0) {
 605                        account.setPgpSignId(0);
 606                        account.unsetPgpSignature();
 607                        xmppConnectionService.databaseBackend.updateAccount(account);
 608                        choosePgpSignId(account);
 609                    } else {
 610                        displayErrorDialog(error);
 611                    }
 612                }
 613            });
 614        }
 615    }
 616
 617    @SuppressWarnings("deprecation")
 618    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
 619    protected void setListItemBackgroundOnView(View view) {
 620        int sdk = android.os.Build.VERSION.SDK_INT;
 621        if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) {
 622            view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground));
 623        } else {
 624            view.setBackground(getResources().getDrawable(R.drawable.greybackground));
 625        }
 626    }
 627
 628    protected void choosePgpSignId(Account account) {
 629        xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<Account>() {
 630            @Override
 631            public void success(Account account1) {
 632            }
 633
 634            @Override
 635            public void error(int errorCode, Account object) {
 636
 637            }
 638
 639            @Override
 640            public void userInputRequired(PendingIntent pi, Account object) {
 641                try {
 642                    startIntentSenderForResult(pi.getIntentSender(),
 643                            REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0);
 644                } catch (final SendIntentException ignored) {
 645                }
 646            }
 647        });
 648    }
 649
 650    protected void displayErrorDialog(final int errorCode) {
 651        runOnUiThread(() -> {
 652            Builder builder = new Builder(XmppActivity.this);
 653            builder.setIconAttribute(android.R.attr.alertDialogIcon);
 654            builder.setTitle(getString(R.string.error));
 655            builder.setMessage(errorCode);
 656            builder.setNeutralButton(R.string.accept, null);
 657            builder.create().show();
 658        });
 659
 660    }
 661
 662    protected void showAddToRosterDialog(final Contact contact) {
 663        AlertDialog.Builder builder = new AlertDialog.Builder(this);
 664        builder.setTitle(contact.getJid().toString());
 665        builder.setMessage(getString(R.string.not_in_roster));
 666        builder.setNegativeButton(getString(R.string.cancel), null);
 667        builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> xmppConnectionService.createContact(contact, true));
 668        builder.create().show();
 669    }
 670
 671    private void showAskForPresenceDialog(final Contact contact) {
 672        AlertDialog.Builder builder = new AlertDialog.Builder(this);
 673        builder.setTitle(contact.getJid().toString());
 674        builder.setMessage(R.string.request_presence_updates);
 675        builder.setNegativeButton(R.string.cancel, null);
 676        builder.setPositiveButton(R.string.request_now,
 677                (dialog, which) -> {
 678                    if (xmppConnectionServiceBound) {
 679                        xmppConnectionService.sendPresencePacket(contact
 680                                .getAccount(), xmppConnectionService
 681                                .getPresenceGenerator()
 682                                .requestPresenceUpdatesFrom(contact));
 683                    }
 684                });
 685        builder.create().show();
 686    }
 687
 688    protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback) {
 689        quickEdit(previousValue, callback, hint, false, false);
 690    }
 691
 692    protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback, boolean permitEmpty) {
 693        quickEdit(previousValue, callback, hint, false, permitEmpty);
 694    }
 695
 696    protected void quickPasswordEdit(String previousValue, OnValueEdited callback) {
 697        quickEdit(previousValue, callback, R.string.password, true, false);
 698    }
 699
 700    @SuppressLint("InflateParams")
 701    private void quickEdit(final String previousValue,
 702                           final OnValueEdited callback,
 703                           final @StringRes int hint,
 704                           boolean password,
 705                           boolean permitEmpty) {
 706        AlertDialog.Builder builder = new AlertDialog.Builder(this);
 707        DialogQuickeditBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_quickedit, null, false);
 708        if (password) {
 709            binding.inputEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
 710        }
 711        builder.setPositiveButton(R.string.accept, null);
 712        if (hint != 0) {
 713            binding.inputLayout.setHint(getString(hint));
 714        }
 715        binding.inputEditText.requestFocus();
 716        if (previousValue != null) {
 717            binding.inputEditText.getText().append(previousValue);
 718        }
 719        builder.setView(binding.getRoot());
 720        builder.setNegativeButton(R.string.cancel, null);
 721        final AlertDialog dialog = builder.create();
 722        dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(binding.inputEditText));
 723        dialog.show();
 724        View.OnClickListener clickListener = v -> {
 725            String value = binding.inputEditText.getText().toString();
 726            if (!value.equals(previousValue) && (!value.trim().isEmpty() || permitEmpty)) {
 727                String error = callback.onValueEdited(value);
 728                if (error != null) {
 729                    binding.inputLayout.setError(error);
 730                    return;
 731                }
 732            }
 733            SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
 734            dialog.dismiss();
 735        };
 736        dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener);
 737        dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v -> {
 738            SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
 739            dialog.dismiss();
 740        }));
 741        dialog.setCanceledOnTouchOutside(false);
 742        dialog.setOnDismissListener(dialog1 -> {
 743            SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
 744        });
 745    }
 746
 747    protected boolean hasStoragePermission(int requestCode) {
 748        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 749            if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
 750                requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
 751                return false;
 752            } else {
 753                return true;
 754            }
 755        } else {
 756            return true;
 757        }
 758    }
 759
 760    protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
 761        super.onActivityResult(requestCode, resultCode, data);
 762        if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
 763            mPendingConferenceInvite = ConferenceInvite.parse(data);
 764            if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
 765                if (mPendingConferenceInvite.execute(this)) {
 766                    mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
 767                    mToast.show();
 768                }
 769                mPendingConferenceInvite = null;
 770            }
 771        }
 772    }
 773
 774    public boolean copyTextToClipboard(String text, int labelResId) {
 775        ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
 776        String label = getResources().getString(labelResId);
 777        if (mClipBoardManager != null) {
 778            ClipData mClipData = ClipData.newPlainText(label, text);
 779            mClipBoardManager.setPrimaryClip(mClipData);
 780            return true;
 781        }
 782        return false;
 783    }
 784
 785    protected boolean manuallyChangePresence() {
 786        return getBooleanPreference(SettingsActivity.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
 787    }
 788
 789    protected String getShareableUri() {
 790        return getShareableUri(false);
 791    }
 792
 793    protected String getShareableUri(boolean http) {
 794        return null;
 795    }
 796
 797    protected void shareLink(boolean http) {
 798        String uri = getShareableUri(http);
 799        if (uri == null || uri.isEmpty()) {
 800            return;
 801        }
 802        Intent intent = new Intent(Intent.ACTION_SEND);
 803        intent.setType("text/plain");
 804        intent.putExtra(Intent.EXTRA_TEXT, getShareableUri(http));
 805        try {
 806            startActivity(Intent.createChooser(intent, getText(R.string.share_uri_with)));
 807        } catch (ActivityNotFoundException e) {
 808            Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
 809        }
 810    }
 811
 812    protected void launchOpenKeyChain(long keyId) {
 813        PgpEngine pgp = XmppActivity.this.xmppConnectionService.getPgpEngine();
 814        try {
 815            startIntentSenderForResult(
 816                    pgp.getIntentForKey(keyId).getIntentSender(), 0, null, 0,
 817                    0, 0);
 818        } catch (Throwable e) {
 819            Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show();
 820        }
 821    }
 822
 823    @Override
 824    protected void onResume(){
 825        super.onResume();
 826        SettingsUtils.applyScreenshotPreventionSetting(this);
 827    }
 828
 829    protected int findTheme() {
 830        return ThemeHelper.find(this);
 831    }
 832
 833    @Override
 834    public void onPause() {
 835        super.onPause();
 836    }
 837
 838    @Override
 839    public boolean onMenuOpened(int id, Menu menu) {
 840        if (id == AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR && menu != null) {
 841            MenuDoubleTabUtil.recordMenuOpen();
 842        }
 843        return super.onMenuOpened(id, menu);
 844    }
 845
 846    protected void showQrCode() {
 847        showQrCode(getShareableUri());
 848    }
 849
 850    protected void showQrCode(final String uri) {
 851        if (uri == null || uri.isEmpty()) {
 852            return;
 853        }
 854        Point size = new Point();
 855        getWindowManager().getDefaultDisplay().getSize(size);
 856        final int width = (size.x < size.y ? size.x : size.y);
 857        Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width);
 858        ImageView view = new ImageView(this);
 859        view.setBackgroundColor(Color.WHITE);
 860        view.setImageBitmap(bitmap);
 861        AlertDialog.Builder builder = new AlertDialog.Builder(this);
 862        builder.setView(view);
 863        builder.create().show();
 864    }
 865
 866    protected Account extractAccount(Intent intent) {
 867        final String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
 868        try {
 869            return jid != null ? xmppConnectionService.findAccountByJid(Jid.ofEscaped(jid)) : null;
 870        } catch (IllegalArgumentException e) {
 871            return null;
 872        }
 873    }
 874
 875    public AvatarService avatarService() {
 876        return xmppConnectionService.getAvatarService();
 877    }
 878
 879    public void loadBitmap(Message message, ImageView imageView) {
 880        Drawable bm;
 881        try {
 882            bm = xmppConnectionService.getFileBackend().getThumbnail(message, getResources(), (int) (metrics.density * 288), true);
 883        } catch (IOException e) {
 884            bm = null;
 885        }
 886        if (bm != null) {
 887            cancelPotentialWork(message, imageView);
 888            imageView.setImageDrawable(bm);
 889            imageView.setBackgroundColor(0x00000000);
 890            if (Build.VERSION.SDK_INT >= 28 && bm instanceof AnimatedImageDrawable) {
 891                ((AnimatedImageDrawable) bm).start();
 892            }
 893        } else {
 894            if (cancelPotentialWork(message, imageView)) {
 895                imageView.setBackgroundColor(0xff333333);
 896                imageView.setImageDrawable(null);
 897                final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
 898                final AsyncDrawable asyncDrawable = new AsyncDrawable(
 899                        getResources(), null, task);
 900                imageView.setImageDrawable(asyncDrawable);
 901                try {
 902                    task.execute(message);
 903                } catch (final RejectedExecutionException ignored) {
 904                    ignored.printStackTrace();
 905                }
 906            }
 907        }
 908    }
 909
 910    protected interface OnValueEdited {
 911        String onValueEdited(String value);
 912    }
 913
 914    public static class ConferenceInvite {
 915        private String uuid;
 916        private final List<Jid> jids = new ArrayList<>();
 917
 918        public static ConferenceInvite parse(Intent data) {
 919            ConferenceInvite invite = new ConferenceInvite();
 920            invite.uuid = data.getStringExtra(ChooseContactActivity.EXTRA_CONVERSATION);
 921            if (invite.uuid == null) {
 922                return null;
 923            }
 924            invite.jids.addAll(ChooseContactActivity.extractJabberIds(data));
 925            return invite;
 926        }
 927
 928        public boolean execute(XmppActivity activity) {
 929            XmppConnectionService service = activity.xmppConnectionService;
 930            Conversation conversation = service.findConversationByUuid(this.uuid);
 931            if (conversation == null) {
 932                return false;
 933            }
 934            if (conversation.getMode() == Conversation.MODE_MULTI) {
 935                for (Jid jid : jids) {
 936                    service.invite(conversation, jid);
 937                }
 938                return false;
 939            } else {
 940                jids.add(conversation.getJid().asBareJid());
 941                return service.createAdhocConference(conversation.getAccount(), null, jids, activity.adhocCallback);
 942            }
 943        }
 944    }
 945
 946    static class BitmapWorkerTask extends AsyncTask<Message, Void, Drawable> {
 947        private final WeakReference<ImageView> imageViewReference;
 948        private Message message = null;
 949
 950        private BitmapWorkerTask(ImageView imageView) {
 951            this.imageViewReference = new WeakReference<>(imageView);
 952        }
 953
 954        @Override
 955        protected Drawable doInBackground(Message... params) {
 956            if (isCancelled()) {
 957                return null;
 958            }
 959            message = params[0];
 960            try {
 961                final XmppActivity activity = find(imageViewReference);
 962                if (activity != null && activity.xmppConnectionService != null) {
 963                    return activity.xmppConnectionService.getFileBackend().getThumbnail(message, imageViewReference.get().getContext().getResources(), (int) (activity.metrics.density * 288), false);
 964                } else {
 965                    return null;
 966                }
 967            } catch (IOException e) {
 968                return null;
 969            }
 970        }
 971
 972        @Override
 973        protected void onPostExecute(final Drawable drawable) {
 974            if (!isCancelled()) {
 975                final ImageView imageView = imageViewReference.get();
 976                if (imageView != null) {
 977                    imageView.setImageDrawable(drawable);
 978                    imageView.setBackgroundColor(drawable == null ? 0xff333333 : 0x00000000);
 979                    if (Build.VERSION.SDK_INT >= 28 && drawable instanceof AnimatedImageDrawable) {
 980                        ((AnimatedImageDrawable) drawable).start();
 981                    }
 982                }
 983            }
 984        }
 985    }
 986
 987    private static class AsyncDrawable extends BitmapDrawable {
 988        private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
 989
 990        private AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
 991            super(res, bitmap);
 992            bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
 993        }
 994
 995        private BitmapWorkerTask getBitmapWorkerTask() {
 996            return bitmapWorkerTaskReference.get();
 997        }
 998    }
 999
1000    public static XmppActivity find(@NonNull WeakReference<ImageView> viewWeakReference) {
1001        final View view = viewWeakReference.get();
1002        return view == null ? null : find(view);
1003    }
1004
1005    public static XmppActivity find(@NonNull final View view) {
1006        Context context = view.getContext();
1007        while (context instanceof ContextWrapper) {
1008            if (context instanceof XmppActivity) {
1009                return (XmppActivity) context;
1010            }
1011            context = ((ContextWrapper) context).getBaseContext();
1012        }
1013        return null;
1014    }
1015}