XmppActivity.java

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