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