XmppActivity.java

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