1package eu.siacs.conversations.ui;
2
3import android.app.Activity;
4import android.app.PendingIntent;
5import android.content.ActivityNotFoundException;
6import android.content.Intent;
7import android.content.IntentSender;
8import android.content.SharedPreferences;
9import android.graphics.Bitmap;
10import android.net.Uri;
11import android.os.Bundle;
12import android.os.Handler;
13import android.preference.PreferenceManager;
14import android.provider.Settings;
15import android.security.KeyChain;
16import android.security.KeyChainAliasCallback;
17import android.text.Editable;
18import android.text.SpannableString;
19import android.text.TextUtils;
20import android.text.TextWatcher;
21import android.text.format.DateUtils;
22import android.text.method.LinkMovementMethod;
23import android.util.Log;
24import android.view.Menu;
25import android.view.MenuItem;
26import android.view.View;
27import android.view.View.OnClickListener;
28import android.widget.CheckBox;
29import android.widget.CompoundButton.OnCheckedChangeListener;
30import android.widget.EditText;
31import android.widget.ImageView;
32import android.widget.TextView;
33import android.widget.Toast;
34import androidx.annotation.NonNull;
35import androidx.appcompat.app.ActionBar;
36import androidx.appcompat.app.AlertDialog;
37import androidx.databinding.DataBindingUtil;
38import androidx.lifecycle.Lifecycle;
39import com.google.android.material.color.MaterialColors;
40import com.google.android.material.dialog.MaterialAlertDialogBuilder;
41import com.google.android.material.textfield.TextInputLayout;
42import com.google.common.base.CharMatcher;
43import com.google.common.base.Strings;
44import de.gultsch.common.Linkify;
45import eu.siacs.conversations.AppSettings;
46import eu.siacs.conversations.Config;
47import eu.siacs.conversations.R;
48import eu.siacs.conversations.crypto.axolotl.AxolotlService;
49import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
50import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
51import eu.siacs.conversations.databinding.ActivityEditAccountBinding;
52import eu.siacs.conversations.databinding.DialogPresenceBinding;
53import eu.siacs.conversations.entities.Account;
54import eu.siacs.conversations.entities.PresenceTemplate;
55import eu.siacs.conversations.services.BarcodeProvider;
56import eu.siacs.conversations.services.QuickConversationsService;
57import eu.siacs.conversations.services.XmppConnectionService;
58import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
59import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested;
60import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
61import eu.siacs.conversations.ui.adapter.PresenceTemplateAdapter;
62import eu.siacs.conversations.ui.text.FixedURLSpan;
63import eu.siacs.conversations.ui.util.AvatarWorkerTask;
64import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
65import eu.siacs.conversations.ui.util.PendingItem;
66import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
67import eu.siacs.conversations.utils.Compatibility;
68import eu.siacs.conversations.utils.CryptoHelper;
69import eu.siacs.conversations.utils.Resolver;
70import eu.siacs.conversations.utils.SignupUtils;
71import eu.siacs.conversations.utils.TorServiceUtils;
72import eu.siacs.conversations.utils.UIHelper;
73import eu.siacs.conversations.utils.XmppUri;
74import eu.siacs.conversations.xml.Element;
75import eu.siacs.conversations.xmpp.Jid;
76import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
77import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
78import eu.siacs.conversations.xmpp.XmppConnection;
79import eu.siacs.conversations.xmpp.XmppConnection.Features;
80import eu.siacs.conversations.xmpp.forms.Data;
81import eu.siacs.conversations.xmpp.pep.Avatar;
82import im.conversations.android.xmpp.model.stanza.Presence;
83import java.util.Arrays;
84import java.util.List;
85import java.util.Set;
86import java.util.concurrent.atomic.AtomicInteger;
87import okhttp3.HttpUrl;
88import org.openintents.openpgp.util.OpenPgpUtils;
89
90public class EditAccountActivity extends OmemoActivity
91 implements OnAccountUpdate,
92 OnUpdateBlocklist,
93 OnKeyStatusUpdated,
94 OnCaptchaRequested,
95 KeyChainAliasCallback,
96 XmppConnectionService.OnShowErrorToast,
97 XmppConnectionService.OnMamPreferencesFetched {
98
99 public static final String EXTRA_OPENED_FROM_NOTIFICATION = "opened_from_notification";
100 public static final String EXTRA_FORCE_REGISTER = "force_register";
101
102 private static final int REQUEST_DATA_SAVER = 0xf244;
103 private static final int REQUEST_CHANGE_STATUS = 0xee11;
104 private static final int REQUEST_ORBOT = 0xff22;
105 private final PendingItem<PresenceTemplate> mPendingPresenceTemplate = new PendingItem<>();
106 private AlertDialog mCaptchaDialog = null;
107 private Jid jidToEdit;
108 private boolean mInitMode = false;
109 private Boolean mForceRegister = null;
110 private boolean mUsernameMode = false;
111 private boolean mShowOptions = false;
112 private Account mAccount;
113 private final OnClickListener mCancelButtonClickListener =
114 v -> {
115 deleteAccountAndReturnIfNecessary();
116 finish();
117 };
118 private final UiCallback<Avatar> mAvatarFetchCallback =
119 new UiCallback<Avatar>() {
120
121 @Override
122 public void userInputRequired(final PendingIntent pi, final Avatar avatar) {
123 finishInitialSetup(avatar);
124 }
125
126 @Override
127 public void success(final Avatar avatar) {
128 finishInitialSetup(avatar);
129 }
130
131 @Override
132 public void error(final int errorCode, final Avatar avatar) {
133 finishInitialSetup(avatar);
134 }
135 };
136 private final OnClickListener mAvatarClickListener =
137 new OnClickListener() {
138 @Override
139 public void onClick(final View view) {
140 if (mAccount != null) {
141 final Intent intent =
142 new Intent(
143 getApplicationContext(),
144 PublishProfilePictureActivity.class);
145 intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toString());
146 startActivity(intent);
147 }
148 }
149 };
150 private String messageFingerprint;
151 private boolean mFetchingAvatar = false;
152 private Toast mFetchingMamPrefsToast;
153 private String mSavedInstanceAccount;
154 private boolean mSavedInstanceInit = false;
155 private XmppUri pendingUri = null;
156 private boolean mUseTor;
157 private ActivityEditAccountBinding binding;
158 private final OnClickListener mSaveButtonClickListener =
159 new OnClickListener() {
160
161 @Override
162 public void onClick(final View v) {
163 final String password = binding.accountPassword.getText().toString();
164 final boolean wasDisabled =
165 mAccount != null && mAccount.getStatus() == Account.State.DISABLED;
166 final boolean accountInfoEdited = accountInfoEdited();
167
168 if (mInitMode && mAccount != null) {
169 mAccount.setOption(Account.OPTION_DISABLED, false);
170 }
171 if (mAccount != null
172 && Arrays.asList(Account.State.DISABLED, Account.State.LOGGED_OUT)
173 .contains(mAccount.getStatus())
174 && !accountInfoEdited) {
175 mAccount.setOption(Account.OPTION_SOFT_DISABLED, false);
176 mAccount.setOption(Account.OPTION_DISABLED, false);
177 if (!xmppConnectionService.updateAccount(mAccount)) {
178 Toast.makeText(
179 EditAccountActivity.this,
180 R.string.unable_to_update_account,
181 Toast.LENGTH_SHORT)
182 .show();
183 }
184 return;
185 }
186 final boolean registerNewAccount;
187 if (mForceRegister != null) {
188 registerNewAccount = mForceRegister;
189 } else {
190 registerNewAccount =
191 binding.accountRegisterNew.isChecked()
192 && !Config.DISALLOW_REGISTRATION_IN_UI;
193 }
194 if (mUsernameMode && binding.accountJid.getText().toString().contains("@")) {
195 binding.accountJidLayout.setError(getString(R.string.invalid_username));
196 removeErrorsOnAllBut(binding.accountJidLayout);
197 binding.accountJid.requestFocus();
198 return;
199 }
200
201 XmppConnection connection =
202 mAccount == null ? null : mAccount.getXmppConnection();
203 final boolean startOrbot =
204 mAccount != null
205 && mAccount.getStatus() == Account.State.TOR_NOT_AVAILABLE;
206 if (startOrbot) {
207 if (TorServiceUtils.isOrbotInstalled(EditAccountActivity.this)) {
208 TorServiceUtils.startOrbot(EditAccountActivity.this, REQUEST_ORBOT);
209 } else {
210 TorServiceUtils.downloadOrbot(EditAccountActivity.this, REQUEST_ORBOT);
211 }
212 return;
213 }
214
215 if (inNeedOfSaslAccept()) {
216 mAccount.resetPinnedMechanism();
217 if (!xmppConnectionService.updateAccount(mAccount)) {
218 Toast.makeText(
219 EditAccountActivity.this,
220 R.string.unable_to_update_account,
221 Toast.LENGTH_SHORT)
222 .show();
223 }
224 return;
225 }
226
227 final boolean openRegistrationUrl =
228 registerNewAccount
229 && !accountInfoEdited
230 && mAccount != null
231 && mAccount.getStatus() == Account.State.REGISTRATION_WEB;
232 final boolean openPaymentUrl =
233 mAccount != null
234 && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED;
235 final boolean redirectionWorthyStatus = openPaymentUrl || openRegistrationUrl;
236 final HttpUrl url =
237 connection != null && redirectionWorthyStatus
238 ? connection.getRedirectionUrl()
239 : null;
240 if (url != null && !wasDisabled) {
241 try {
242 startActivity(
243 new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString())));
244 return;
245 } catch (ActivityNotFoundException e) {
246 Toast.makeText(
247 EditAccountActivity.this,
248 R.string.application_found_to_open_website,
249 Toast.LENGTH_SHORT)
250 .show();
251 return;
252 }
253 }
254
255 final Jid jid;
256 try {
257 if (mUsernameMode) {
258 jid =
259 Jid.of(
260 binding.accountJid.getText().toString(),
261 getUserModeDomain(),
262 null);
263 } else {
264 jid = Jid.ofUserInput(binding.accountJid.getText().toString());
265 Resolver.checkDomain(jid);
266 }
267 } catch (final NullPointerException | IllegalArgumentException e) {
268 if (mUsernameMode) {
269 binding.accountJidLayout.setError(getString(R.string.invalid_username));
270 } else {
271 binding.accountJidLayout.setError(getString(R.string.invalid_jid));
272 }
273 binding.accountJid.requestFocus();
274 removeErrorsOnAllBut(binding.accountJidLayout);
275 return;
276 }
277 final String hostname;
278 int numericPort = 5222;
279 if (mShowOptions) {
280 hostname = CharMatcher.whitespace().removeFrom(binding.hostname.getText());
281 final String port =
282 CharMatcher.whitespace().removeFrom(binding.port.getText());
283 if (Resolver.invalidHostname(hostname)) {
284 binding.hostnameLayout.setError(getString(R.string.not_valid_hostname));
285 binding.hostname.requestFocus();
286 removeErrorsOnAllBut(binding.hostnameLayout);
287 return;
288 }
289 if (!hostname.isEmpty()) {
290 try {
291 numericPort = Integer.parseInt(port);
292 if (numericPort < 0 || numericPort > 65535) {
293 binding.portLayout.setError(
294 getString(R.string.not_a_valid_port));
295 removeErrorsOnAllBut(binding.portLayout);
296 binding.port.requestFocus();
297 return;
298 }
299
300 } catch (NumberFormatException e) {
301 binding.portLayout.setError(getString(R.string.not_a_valid_port));
302 removeErrorsOnAllBut(binding.portLayout);
303 binding.port.requestFocus();
304 return;
305 }
306 }
307 } else {
308 hostname = null;
309 }
310
311 if (jid.getLocal() == null) {
312 if (mUsernameMode) {
313 binding.accountJidLayout.setError(getString(R.string.invalid_username));
314 } else {
315 binding.accountJidLayout.setError(getString(R.string.invalid_jid));
316 }
317 removeErrorsOnAllBut(binding.accountJidLayout);
318 binding.accountJid.requestFocus();
319 return;
320 }
321 if (mAccount != null) {
322 if (mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) {
323 mAccount.setOption(
324 Account.OPTION_MAGIC_CREATE,
325 mAccount.getPassword().contains(password));
326 }
327 mAccount.setJid(jid);
328 mAccount.setPort(numericPort);
329 mAccount.setHostname(hostname);
330 binding.accountJidLayout.setError(null);
331 mAccount.setPassword(password);
332 mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
333 if (!xmppConnectionService.updateAccount(mAccount)) {
334 Toast.makeText(
335 EditAccountActivity.this,
336 R.string.unable_to_update_account,
337 Toast.LENGTH_SHORT)
338 .show();
339 return;
340 }
341 } else {
342 if (xmppConnectionService.findAccountByJid(jid) != null) {
343 binding.accountJidLayout.setError(
344 getString(R.string.account_already_exists));
345 removeErrorsOnAllBut(binding.accountJidLayout);
346 binding.accountJid.requestFocus();
347 return;
348 }
349 mAccount = new Account(jid.asBareJid(), password);
350 mAccount.setPort(numericPort);
351 mAccount.setHostname(hostname);
352 mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
353 xmppConnectionService.createAccount(mAccount);
354 }
355 binding.hostnameLayout.setError(null);
356 binding.portLayout.setError(null);
357 if (mAccount.isOnion()) {
358 Toast.makeText(
359 EditAccountActivity.this,
360 R.string.audio_video_disabled_tor,
361 Toast.LENGTH_LONG)
362 .show();
363 }
364 if (mAccount.isEnabled() && !registerNewAccount && !mInitMode) {
365 finish();
366 } else {
367 updateSaveButton();
368 updateAccountInformation(true);
369 }
370 }
371 };
372 private final TextWatcher mTextWatcher =
373 new TextWatcher() {
374
375 @Override
376 public void onTextChanged(
377 final CharSequence s, final int start, final int before, final int count) {
378 updatePortLayout();
379 updateSaveButton();
380 }
381
382 @Override
383 public void beforeTextChanged(
384 final CharSequence s, final int start, final int count, final int after) {}
385
386 @Override
387 public void afterTextChanged(final Editable s) {}
388 };
389 private final View.OnFocusChangeListener mEditTextFocusListener =
390 new View.OnFocusChangeListener() {
391 @Override
392 public void onFocusChange(View view, boolean b) {
393 EditText et = (EditText) view;
394 if (b) {
395 int resId =
396 mUsernameMode
397 ? R.string.username
398 : R.string.account_settings_example_jabber_id;
399 if (view.getId() == R.id.hostname) {
400 resId =
401 mUseTor
402 ? R.string.hostname_or_onion
403 : R.string.hostname_example;
404 }
405 final int res = resId;
406 new Handler().postDelayed(() -> et.setHint(res), 200);
407 } else {
408 et.setHint(null);
409 }
410 }
411 };
412
413 private static void setAvailabilityRadioButton(
414 Presence.Availability status, DialogPresenceBinding binding) {
415 if (status == null) {
416 binding.online.setChecked(true);
417 return;
418 }
419 switch (status) {
420 case DND:
421 binding.dnd.setChecked(true);
422 break;
423 case XA:
424 binding.xa.setChecked(true);
425 break;
426 case AWAY:
427 binding.away.setChecked(true);
428 break;
429 default:
430 binding.online.setChecked(true);
431 }
432 }
433
434 private static Presence.Availability getAvailabilityRadioButton(DialogPresenceBinding binding) {
435 if (binding.dnd.isChecked()) {
436 return Presence.Availability.DND;
437 } else if (binding.xa.isChecked()) {
438 return Presence.Availability.XA;
439 } else if (binding.away.isChecked()) {
440 return Presence.Availability.AWAY;
441 } else {
442 return Presence.Availability.ONLINE;
443 }
444 }
445
446 public void refreshUiReal() {
447 invalidateOptionsMenu();
448 if (mAccount != null && mAccount.getStatus() != Account.State.ONLINE && mFetchingAvatar) {
449 Intent intent = new Intent(this, StartConversationActivity.class);
450 StartConversationActivity.addInviteUri(intent, getIntent());
451 startActivity(intent);
452 finish();
453 } else if (mInitMode && mAccount != null && mAccount.getStatus() == Account.State.ONLINE) {
454 if (!mFetchingAvatar) {
455 mFetchingAvatar = true;
456 xmppConnectionService.checkForAvatar(mAccount, mAvatarFetchCallback);
457 }
458 }
459 if (mAccount != null) {
460 updateAccountInformation(false);
461 }
462 updateSaveButton();
463 }
464
465 @Override
466 public boolean onNavigateUp() {
467 deleteAccountAndReturnIfNecessary();
468 return super.onNavigateUp();
469 }
470
471 @Override
472 public void onBackPressed() {
473 deleteAccountAndReturnIfNecessary();
474 super.onBackPressed();
475 }
476
477 private void deleteAccountAndReturnIfNecessary() {
478 if (mInitMode
479 && mAccount != null
480 && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)) {
481 xmppConnectionService.deleteAccount(mAccount);
482 }
483
484 final boolean magicCreate =
485 mAccount != null
486 && mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)
487 && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
488 final Jid jid = mAccount == null ? null : mAccount.getJid();
489
490 if (SignupUtils.isSupportTokenRegistry()
491 && jid != null
492 && magicCreate
493 && !jid.getDomain().equals(Config.MAGIC_CREATE_DOMAIN)) {
494 final Jid preset;
495 if (mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME)) {
496 preset = jid.asBareJid();
497 } else {
498 preset = jid.getDomain();
499 }
500 final Intent intent =
501 SignupUtils.getTokenRegistrationIntent(
502 this, preset, mAccount.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN));
503 StartConversationActivity.addInviteUri(intent, getIntent());
504 startActivity(intent);
505 return;
506 }
507
508 final List<Account> accounts =
509 xmppConnectionService == null ? null : xmppConnectionService.getAccounts();
510 if (accounts != null && accounts.isEmpty() && Config.MAGIC_CREATE_DOMAIN != null) {
511 Intent intent =
512 SignupUtils.getSignUpIntent(this, mForceRegister != null && mForceRegister);
513 StartConversationActivity.addInviteUri(intent, getIntent());
514 startActivity(intent);
515 }
516 }
517
518 @Override
519 public void onAccountUpdate() {
520 refreshUi();
521 }
522
523 protected void finishInitialSetup(final Avatar avatar) {
524 runOnUiThread(
525 () -> {
526 SoftKeyboardUtils.hideSoftKeyboard(EditAccountActivity.this);
527 final Intent intent;
528 final XmppConnection connection = mAccount.getXmppConnection();
529 final boolean wasFirstAccount =
530 xmppConnectionService != null
531 && xmppConnectionService.getAccounts().size() == 1;
532 if (avatar != null || (connection != null && !connection.getFeatures().pep())) {
533 intent =
534 new Intent(
535 getApplicationContext(), StartConversationActivity.class);
536 if (wasFirstAccount) {
537 intent.putExtra("init", true);
538 }
539 intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toString());
540 } else {
541 intent =
542 new Intent(
543 getApplicationContext(),
544 PublishProfilePictureActivity.class);
545 intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toString());
546 intent.putExtra("setup", true);
547 }
548 if (wasFirstAccount) {
549 intent.setFlags(
550 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
551 }
552 StartConversationActivity.addInviteUri(intent, getIntent());
553 startActivity(intent);
554 finish();
555 });
556 }
557
558 @Override
559 public void onActivityResult(int requestCode, int resultCode, Intent data) {
560 // TODO check for Camera / Scan permission
561 super.onActivityResult(requestCode, resultCode, data);
562 if (requestCode == REQUEST_BATTERY_OP || requestCode == REQUEST_DATA_SAVER) {
563 updateAccountInformation(mAccount == null);
564 }
565 if (requestCode == REQUEST_BATTERY_OP) {
566 // the result code is always 0 even when battery permission were granted
567 XmppConnectionService.toggleForegroundService(xmppConnectionService);
568 }
569 if (requestCode == REQUEST_CHANGE_STATUS) {
570 PresenceTemplate template = mPendingPresenceTemplate.pop();
571 if (template != null && resultCode == Activity.RESULT_OK) {
572 generateSignature(data, template);
573 } else {
574 Log.d(Config.LOGTAG, "pgp result not ok");
575 }
576 }
577 }
578
579 @Override
580 protected void processFingerprintVerification(XmppUri uri) {
581 processFingerprintVerification(uri, true);
582 }
583
584 protected void processFingerprintVerification(XmppUri uri, boolean showWarningToast) {
585 if (mAccount != null
586 && mAccount.getJid().asBareJid().equals(uri.getJid())
587 && uri.hasFingerprints()) {
588 if (xmppConnectionService.verifyFingerprints(mAccount, uri.getFingerprints())) {
589 Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
590 updateAccountInformation(false);
591 }
592 } else if (showWarningToast) {
593 Toast.makeText(this, R.string.invalid_barcode, Toast.LENGTH_SHORT).show();
594 }
595 }
596
597 private void updatePortLayout() {
598 final String hostname = this.binding.hostname.getText().toString();
599 if (TextUtils.isEmpty(hostname)) {
600 this.binding.portLayout.setEnabled(false);
601 this.binding.portLayout.setError(null);
602 } else {
603 this.binding.portLayout.setEnabled(true);
604 }
605 }
606
607 protected void updateSaveButton() {
608 boolean accountInfoEdited = accountInfoEdited();
609
610 if (accountInfoEdited && !mInitMode) {
611 this.binding.saveButton.setText(R.string.save);
612 this.binding.saveButton.setEnabled(true);
613 } else if (mAccount != null
614 && (mAccount.getStatus() == Account.State.CONNECTING
615 || mAccount.getStatus() == Account.State.REGISTRATION_SUCCESSFUL
616 || mFetchingAvatar)) {
617 this.binding.saveButton.setEnabled(false);
618 this.binding.saveButton.setText(R.string.account_status_connecting);
619 } else if (mAccount != null
620 && mAccount.getStatus() == Account.State.DISABLED
621 && !mInitMode) {
622 this.binding.saveButton.setEnabled(true);
623 this.binding.saveButton.setText(R.string.enable);
624 } else if (torNeedsInstall(mAccount)) {
625 this.binding.saveButton.setEnabled(true);
626 this.binding.saveButton.setText(R.string.install_orbot);
627 } else if (torNeedsStart(mAccount)) {
628 this.binding.saveButton.setEnabled(true);
629 this.binding.saveButton.setText(R.string.start_orbot);
630 } else {
631 this.binding.saveButton.setEnabled(true);
632 if (!mInitMode) {
633 if (mAccount != null && mAccount.isOnlineAndConnected()) {
634 this.binding.saveButton.setText(R.string.save);
635 if (!accountInfoEdited) {
636 this.binding.saveButton.setEnabled(false);
637 }
638 } else {
639 XmppConnection connection =
640 mAccount == null ? null : mAccount.getXmppConnection();
641 HttpUrl url =
642 connection != null
643 && mAccount.getStatus()
644 == Account.State.PAYMENT_REQUIRED
645 ? connection.getRedirectionUrl()
646 : null;
647 if (url != null) {
648 this.binding.saveButton.setText(R.string.open_website);
649 } else if (inNeedOfSaslAccept()) {
650 this.binding.saveButton.setText(R.string.accept);
651 } else {
652 this.binding.saveButton.setText(R.string.connect);
653 }
654 }
655 } else {
656 XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection();
657 HttpUrl url =
658 connection != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB
659 ? connection.getRedirectionUrl()
660 : null;
661 if (url != null
662 && this.binding.accountRegisterNew.isChecked()
663 && !accountInfoEdited) {
664 this.binding.saveButton.setText(R.string.open_website);
665 } else {
666 this.binding.saveButton.setText(R.string.next);
667 }
668 }
669 }
670 }
671
672 private boolean torNeedsInstall(final Account account) {
673 return account != null
674 && account.getStatus() == Account.State.TOR_NOT_AVAILABLE
675 && !TorServiceUtils.isOrbotInstalled(this);
676 }
677
678 private boolean torNeedsStart(final Account account) {
679 return account != null && account.getStatus() == Account.State.TOR_NOT_AVAILABLE;
680 }
681
682 protected boolean accountInfoEdited() {
683 if (this.mAccount == null) {
684 return false;
685 }
686 return jidEdited()
687 || !this.mAccount
688 .getPassword()
689 .equals(this.binding.accountPassword.getText().toString())
690 || !this.mAccount.getHostname().equals(this.binding.hostname.getText().toString())
691 || !String.valueOf(this.mAccount.getPort())
692 .equals(this.binding.port.getText().toString());
693 }
694
695 protected boolean jidEdited() {
696 final String unmodified;
697 if (mUsernameMode) {
698 unmodified = this.mAccount.getJid().getLocal();
699 } else {
700 unmodified = this.mAccount.getJid().asBareJid().toString();
701 }
702 return !unmodified.equals(this.binding.accountJid.getText().toString());
703 }
704
705 @Override
706 protected String getShareableUri(boolean http) {
707 if (mAccount != null) {
708 return http ? mAccount.getShareableLink() : mAccount.getShareableUri();
709 } else {
710 return null;
711 }
712 }
713
714 @Override
715 protected void onCreate(final Bundle savedInstanceState) {
716 super.onCreate(savedInstanceState);
717 if (savedInstanceState != null) {
718 this.mSavedInstanceAccount = savedInstanceState.getString("account");
719 this.mSavedInstanceInit = savedInstanceState.getBoolean("initMode", false);
720 }
721 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_edit_account);
722 Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
723 setSupportActionBar(binding.toolbar);
724 binding.accountJid.addTextChangedListener(this.mTextWatcher);
725 binding.accountJid.setOnFocusChangeListener(this.mEditTextFocusListener);
726 this.binding.accountPassword.addTextChangedListener(this.mTextWatcher);
727
728 this.binding.avater.setOnClickListener(this.mAvatarClickListener);
729 this.binding.hostname.addTextChangedListener(mTextWatcher);
730 this.binding.hostname.setOnFocusChangeListener(mEditTextFocusListener);
731 this.binding.clearDevices.setOnClickListener(v -> showWipePepDialog());
732 this.binding.port.setText(String.valueOf(Resolver.XMPP_PORT_STARTTLS));
733 this.binding.port.addTextChangedListener(mTextWatcher);
734 this.binding.saveButton.setOnClickListener(this.mSaveButtonClickListener);
735 this.binding.cancelButton.setOnClickListener(this.mCancelButtonClickListener);
736 if (savedInstanceState != null && savedInstanceState.getBoolean("showMoreTable")) {
737 changeMoreTableVisibility(true);
738 }
739 final OnCheckedChangeListener OnCheckedShowConfirmPassword =
740 (buttonView, isChecked) -> updateSaveButton();
741 this.binding.accountRegisterNew.setOnCheckedChangeListener(OnCheckedShowConfirmPassword);
742 if (Config.DISALLOW_REGISTRATION_IN_UI) {
743 this.binding.accountRegisterNew.setVisibility(View.GONE);
744 }
745 this.binding.actionEditYourName.setOnClickListener(this::onEditYourNameClicked);
746 this.binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this));
747 }
748
749 private void onEditYourNameClicked(View view) {
750 quickEdit(
751 mAccount.getDisplayName(),
752 R.string.your_name,
753 value -> {
754 final String displayName = value.trim();
755 updateDisplayName(displayName);
756 mAccount.setDisplayName(displayName);
757 xmppConnectionService.publishDisplayName(mAccount);
758 refreshAvatar();
759 return null;
760 },
761 true);
762 }
763
764 private void refreshAvatar() {
765 AvatarWorkerTask.loadAvatar(
766 mAccount, binding.avater, R.dimen.avatar_on_details_screen_size);
767 }
768
769 @Override
770 public boolean onCreateOptionsMenu(final Menu menu) {
771 super.onCreateOptionsMenu(menu);
772 getMenuInflater().inflate(R.menu.editaccount, menu);
773 final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list);
774 final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
775 final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server);
776 final MenuItem deleteAccount = menu.findItem(R.id.action_delete_account);
777 final MenuItem renewCertificate = menu.findItem(R.id.action_renew_certificate);
778 final MenuItem mamPrefs = menu.findItem(R.id.action_mam_prefs);
779 final MenuItem changePresence = menu.findItem(R.id.action_change_presence);
780 final MenuItem share = menu.findItem(R.id.action_share);
781 renewCertificate.setVisible(mAccount != null && mAccount.getPrivateKeyAlias() != null);
782
783 share.setVisible(mAccount != null && !mInitMode);
784
785 if (mAccount != null && mAccount.isOnlineAndConnected()) {
786 if (!mAccount.getXmppConnection().getFeatures().blocking()) {
787 showBlocklist.setVisible(false);
788 }
789
790 if (!mAccount.getXmppConnection().getFeatures().register()) {
791 changePassword.setVisible(false);
792 deleteAccount.setVisible(false);
793 }
794 mamPrefs.setVisible(mAccount.getXmppConnection().getFeatures().mam());
795 changePresence.setVisible(!mInitMode);
796 } else {
797 showBlocklist.setVisible(false);
798 showMoreInfo.setVisible(false);
799 changePassword.setVisible(false);
800 deleteAccount.setVisible(false);
801 mamPrefs.setVisible(false);
802 changePresence.setVisible(false);
803 }
804 return super.onCreateOptionsMenu(menu);
805 }
806
807 @Override
808 public boolean onPrepareOptionsMenu(Menu menu) {
809 final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
810 if (showMoreInfo.isVisible()) {
811 showMoreInfo.setChecked(binding.serverInfoMore.getVisibility() == View.VISIBLE);
812 }
813 return super.onPrepareOptionsMenu(menu);
814 }
815
816 @Override
817 public void onStart() {
818 super.onStart();
819 final Intent intent = getIntent();
820 if (intent != null) {
821 try {
822 this.jidToEdit = Jid.of(intent.getStringExtra("jid"));
823 } catch (final IllegalArgumentException | NullPointerException ignored) {
824 this.jidToEdit = null;
825 }
826 final Uri data = intent.getData();
827 final XmppUri xmppUri = data == null ? null : new XmppUri(data);
828 final boolean scanned = intent.getBooleanExtra("scanned", false);
829 if (jidToEdit != null && xmppUri != null && xmppUri.hasFingerprints()) {
830 if (scanned) {
831 if (xmppConnectionServiceBound) {
832 processFingerprintVerification(xmppUri, false);
833 } else {
834 this.pendingUri = xmppUri;
835 }
836 } else {
837 displayVerificationWarningDialog(xmppUri);
838 }
839 }
840 boolean init = intent.getBooleanExtra("init", false);
841 boolean openedFromNotification =
842 intent.getBooleanExtra(EXTRA_OPENED_FROM_NOTIFICATION, false);
843 Log.d(Config.LOGTAG, "extras " + intent.getExtras());
844 this.mForceRegister =
845 intent.hasExtra(EXTRA_FORCE_REGISTER)
846 ? intent.getBooleanExtra(EXTRA_FORCE_REGISTER, false)
847 : null;
848 Log.d(Config.LOGTAG, "force register=" + mForceRegister);
849 this.mInitMode = init || this.jidToEdit == null;
850 this.messageFingerprint = intent.getStringExtra("fingerprint");
851 if (!mInitMode) {
852 this.binding.accountRegisterNew.setVisibility(View.GONE);
853 setTitle(getString(R.string.account_details));
854 configureActionBar(getSupportActionBar(), !openedFromNotification);
855 } else {
856 this.binding.avater.setVisibility(View.GONE);
857 configureActionBar(
858 getSupportActionBar(), !(init && Config.MAGIC_CREATE_DOMAIN == null));
859 if (mForceRegister != null) {
860 if (mForceRegister) {
861 setTitle(R.string.register_new_account);
862 } else {
863 setTitle(R.string.add_existing_account);
864 }
865 } else {
866 setTitle(R.string.action_add_account);
867 }
868 }
869 }
870 SharedPreferences preferences = getPreferences();
871 mUseTor =
872 QuickConversationsService.isConversations()
873 && preferences.getBoolean(
874 "use_tor", getResources().getBoolean(R.bool.use_tor));
875 this.mShowOptions =
876 mUseTor
877 || (QuickConversationsService.isConversations()
878 && preferences.getBoolean(
879 "show_connection_options",
880 getResources().getBoolean(R.bool.show_connection_options)));
881 this.binding.namePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE);
882 if (mForceRegister != null) {
883 this.binding.accountRegisterNew.setVisibility(View.GONE);
884 }
885 }
886
887 private void displayVerificationWarningDialog(final XmppUri xmppUri) {
888 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
889 builder.setTitle(R.string.verify_omemo_keys);
890 View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
891 final CheckBox isTrustedSource = view.findViewById(R.id.trusted_source);
892 TextView warning = view.findViewById(R.id.warning);
893 warning.setText(R.string.verifying_omemo_keys_trusted_source_account);
894 builder.setView(view);
895 builder.setPositiveButton(
896 R.string.continue_btn,
897 (dialog, which) -> {
898 if (isTrustedSource.isChecked()) {
899 processFingerprintVerification(xmppUri, false);
900 } else {
901 finish();
902 }
903 });
904 builder.setNegativeButton(R.string.cancel, (dialog, which) -> finish());
905 final var dialog = builder.create();
906 dialog.setCanceledOnTouchOutside(false);
907 dialog.setOnCancelListener(d -> finish());
908 dialog.show();
909 }
910
911 @Override
912 public void onNewIntent(@NonNull final Intent intent) {
913 super.onNewIntent(intent);
914 if (intent.getData() != null) {
915 final XmppUri uri = new XmppUri(intent.getData());
916 if (xmppConnectionServiceBound) {
917 processFingerprintVerification(uri, false);
918 } else {
919 this.pendingUri = uri;
920 }
921 }
922 }
923
924 @Override
925 public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) {
926 if (mAccount != null) {
927 savedInstanceState.putString("account", mAccount.getJid().asBareJid().toString());
928 savedInstanceState.putBoolean("initMode", mInitMode);
929 savedInstanceState.putBoolean(
930 "showMoreTable", binding.serverInfoMore.getVisibility() == View.VISIBLE);
931 }
932 super.onSaveInstanceState(savedInstanceState);
933 }
934
935 protected void onBackendConnected() {
936 boolean init = true;
937 if (mSavedInstanceAccount != null) {
938 try {
939 this.mAccount =
940 xmppConnectionService.findAccountByJid(Jid.of(mSavedInstanceAccount));
941 this.mInitMode = mSavedInstanceInit;
942 init = false;
943 } catch (IllegalArgumentException e) {
944 this.mAccount = null;
945 }
946
947 } else if (this.jidToEdit != null) {
948 this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit);
949 }
950
951 if (mAccount != null) {
952 this.mInitMode |= this.mAccount.isOptionSet(Account.OPTION_REGISTER);
953 this.mUsernameMode |=
954 mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)
955 && mAccount.isOptionSet(Account.OPTION_REGISTER);
956 if (mPendingFingerprintVerificationUri != null) {
957 processFingerprintVerification(mPendingFingerprintVerificationUri, false);
958 mPendingFingerprintVerificationUri = null;
959 }
960 updateAccountInformation(init);
961 }
962
963 if (Config.MAGIC_CREATE_DOMAIN == null
964 && this.xmppConnectionService.getAccounts().size() == 0) {
965 this.binding.cancelButton.setEnabled(false);
966 }
967 if (mUsernameMode) {
968 this.binding.accountJidLayout.setHint(getString(R.string.username_hint));
969 } else {
970 final KnownHostsAdapter mKnownHostsAdapter =
971 new KnownHostsAdapter(
972 this,
973 R.layout.item_autocomplete,
974 xmppConnectionService.getKnownHosts());
975 this.binding.accountJid.setAdapter(mKnownHostsAdapter);
976 }
977
978 if (pendingUri != null) {
979 processFingerprintVerification(pendingUri, false);
980 pendingUri = null;
981 }
982 updatePortLayout();
983 updateSaveButton();
984 invalidateOptionsMenu();
985 }
986
987 private String getUserModeDomain() {
988 if (mAccount != null && mAccount.getJid().getDomain() != null) {
989 return mAccount.getServer();
990 } else {
991 return null;
992 }
993 }
994
995 @Override
996 public boolean onOptionsItemSelected(final MenuItem item) {
997 if (MenuDoubleTabUtil.shouldIgnoreTap()) {
998 return false;
999 }
1000 switch (item.getItemId()) {
1001 case android.R.id.home:
1002 deleteAccountAndReturnIfNecessary();
1003 break;
1004 case R.id.action_show_block_list:
1005 final Intent showBlocklistIntent = new Intent(this, BlocklistActivity.class);
1006 showBlocklistIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toString());
1007 startActivity(showBlocklistIntent);
1008 break;
1009 case R.id.action_server_info_show_more:
1010 changeMoreTableVisibility(!item.isChecked());
1011 break;
1012 case R.id.action_share_barcode:
1013 shareBarcode();
1014 break;
1015 case R.id.action_share_http:
1016 shareLink(true);
1017 break;
1018 case R.id.action_share_uri:
1019 shareLink(false);
1020 break;
1021 case R.id.action_change_password_on_server:
1022 gotoChangePassword();
1023 break;
1024 case R.id.action_delete_account:
1025 deleteAccount();
1026 break;
1027 case R.id.action_mam_prefs:
1028 editMamPrefs();
1029 break;
1030 case R.id.action_renew_certificate:
1031 renewCertificate();
1032 break;
1033 case R.id.action_change_presence:
1034 changePresence();
1035 break;
1036 }
1037 return super.onOptionsItemSelected(item);
1038 }
1039
1040 private void deleteAccount() {
1041 this.deleteAccount(mAccount, () -> finish());
1042 }
1043
1044 private boolean inNeedOfSaslAccept() {
1045 return mAccount != null
1046 && mAccount.getLastErrorStatus() == Account.State.DOWNGRADE_ATTACK
1047 && mAccount.getPinnedMechanismPriority() >= 0
1048 && !accountInfoEdited();
1049 }
1050
1051 private void shareBarcode() {
1052 Intent intent = new Intent(Intent.ACTION_SEND);
1053 intent.putExtra(Intent.EXTRA_STREAM, BarcodeProvider.getUriForAccount(this, mAccount));
1054 intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1055 intent.setType("image/png");
1056 startActivity(Intent.createChooser(intent, getText(R.string.share_with)));
1057 }
1058
1059 private void changeMoreTableVisibility(final boolean visible) {
1060 binding.serverInfoMore.setVisibility(visible ? View.VISIBLE : View.GONE);
1061 binding.serverInfoLoginMechanism.setVisibility(visible ? View.VISIBLE : View.GONE);
1062 }
1063
1064 private void gotoChangePassword() {
1065 final Intent changePasswordIntent = new Intent(this, ChangePasswordActivity.class);
1066 changePasswordIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toString());
1067 startActivity(changePasswordIntent);
1068 }
1069
1070 private void renewCertificate() {
1071 KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
1072 }
1073
1074 private void changePresence() {
1075 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1076 boolean manualStatus =
1077 sharedPreferences.getBoolean(
1078 AppSettings.MANUALLY_CHANGE_PRESENCE,
1079 getResources().getBoolean(R.bool.manually_change_presence));
1080 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1081 final DialogPresenceBinding binding =
1082 DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_presence, null, false);
1083 String current = mAccount.getPresenceStatusMessage();
1084 if (current != null && !current.trim().isEmpty()) {
1085 binding.statusMessage.append(current);
1086 }
1087 setAvailabilityRadioButton(mAccount.getPresenceStatus(), binding);
1088 binding.show.setVisibility(manualStatus ? View.VISIBLE : View.GONE);
1089 List<PresenceTemplate> templates = xmppConnectionService.getPresenceTemplates(mAccount);
1090 PresenceTemplateAdapter presenceTemplateAdapter =
1091 new PresenceTemplateAdapter(this, R.layout.item_autocomplete, templates);
1092 binding.statusMessage.setAdapter(presenceTemplateAdapter);
1093 binding.statusMessage.setOnItemClickListener(
1094 (parent, view, position, id) -> {
1095 PresenceTemplate template =
1096 (PresenceTemplate) parent.getItemAtPosition(position);
1097 setAvailabilityRadioButton(template.getStatus(), binding);
1098 });
1099 builder.setTitle(R.string.edit_status_message_title);
1100 builder.setView(binding.getRoot());
1101 builder.setNegativeButton(R.string.cancel, null);
1102 builder.setPositiveButton(
1103 R.string.confirm,
1104 (dialog, which) -> {
1105 PresenceTemplate template =
1106 new PresenceTemplate(
1107 getAvailabilityRadioButton(binding),
1108 binding.statusMessage.getText().toString().trim());
1109 if (mAccount.getPgpId() != 0 && hasPgp()) {
1110 generateSignature(null, template);
1111 } else {
1112 xmppConnectionService.changeStatus(mAccount, template, null);
1113 }
1114 });
1115 builder.create().show();
1116 }
1117
1118 private void generateSignature(Intent intent, PresenceTemplate template) {
1119 xmppConnectionService
1120 .getPgpEngine()
1121 .generateSignature(
1122 intent,
1123 mAccount,
1124 template.getStatusMessage(),
1125 new UiCallback<String>() {
1126 @Override
1127 public void success(String signature) {
1128 xmppConnectionService.changeStatus(mAccount, template, signature);
1129 }
1130
1131 @Override
1132 public void error(int errorCode, String object) {}
1133
1134 @Override
1135 public void userInputRequired(PendingIntent pi, String object) {
1136 mPendingPresenceTemplate.push(template);
1137 try {
1138 startIntentSenderForResult(
1139 pi.getIntentSender(),
1140 REQUEST_CHANGE_STATUS,
1141 null,
1142 0,
1143 0,
1144 0,
1145 Compatibility.pgpStartIntentSenderOptions());
1146 } catch (final IntentSender.SendIntentException ignored) {
1147 }
1148 }
1149 });
1150 }
1151
1152 @Override
1153 public void alias(String alias) {
1154 if (alias != null) {
1155 xmppConnectionService.updateKeyInAccount(mAccount, alias);
1156 }
1157 }
1158
1159 private void updateAccountInformation(boolean init) {
1160 if (init) {
1161 this.binding.accountJid.getEditableText().clear();
1162 if (mUsernameMode) {
1163 this.binding.accountJid.getEditableText().append(this.mAccount.getJid().getLocal());
1164 } else {
1165 this.binding
1166 .accountJid
1167 .getEditableText()
1168 .append(this.mAccount.getJid().asBareJid().toString());
1169 }
1170 this.binding.accountPassword.getEditableText().clear();
1171 this.binding.accountPassword.getEditableText().append(this.mAccount.getPassword());
1172 this.binding.hostname.setText("");
1173 this.binding.hostname.getEditableText().append(this.mAccount.getHostname());
1174 this.binding.port.setText("");
1175 this.binding.port.getEditableText().append(String.valueOf(this.mAccount.getPort()));
1176 this.binding.namePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE);
1177 }
1178
1179 final boolean editable =
1180 !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)
1181 && !mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME)
1182 && QuickConversationsService.isConversations();
1183 this.binding.accountJid.setEnabled(editable);
1184 this.binding.accountJid.setFocusable(editable);
1185 this.binding.accountJid.setFocusableInTouchMode(editable);
1186 this.binding.accountJid.setCursorVisible(editable);
1187
1188 final String displayName = mAccount.getDisplayName();
1189 updateDisplayName(displayName);
1190
1191 final boolean togglePassword =
1192 mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)
1193 || !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
1194 final boolean neverLoggedIn =
1195 !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)
1196 && QuickConversationsService.isConversations();
1197 final boolean editPassword = mAccount.unauthorized() || neverLoggedIn;
1198
1199 this.binding.accountPasswordLayout.setPasswordVisibilityToggleEnabled(togglePassword);
1200
1201 this.binding.accountPassword.setFocusable(editPassword);
1202 this.binding.accountPassword.setFocusableInTouchMode(editPassword);
1203 this.binding.accountPassword.setCursorVisible(editPassword);
1204 this.binding.accountPassword.setEnabled(editPassword);
1205
1206 if (!mInitMode) {
1207 this.binding.avater.setVisibility(View.VISIBLE);
1208 AvatarWorkerTask.loadAvatar(
1209 mAccount, binding.avater, R.dimen.avatar_on_details_screen_size);
1210 } else {
1211 this.binding.avater.setVisibility(View.GONE);
1212 }
1213 this.binding.accountRegisterNew.setChecked(
1214 this.mAccount.isOptionSet(Account.OPTION_REGISTER));
1215 if (this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) {
1216 if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) {
1217 ActionBar actionBar = getSupportActionBar();
1218 if (actionBar != null) {
1219 actionBar.setTitle(R.string.create_account);
1220 }
1221 }
1222 this.binding.accountRegisterNew.setVisibility(View.GONE);
1223 } else if (this.mAccount.isOptionSet(Account.OPTION_REGISTER) && mForceRegister == null) {
1224 this.binding.accountRegisterNew.setVisibility(View.VISIBLE);
1225 } else {
1226 this.binding.accountRegisterNew.setVisibility(View.GONE);
1227 }
1228 if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) {
1229 final Features features = this.mAccount.getXmppConnection().getFeatures();
1230 this.binding.stats.setVisibility(View.VISIBLE);
1231 boolean showBatteryWarning = isOptimizingBattery();
1232 boolean showDataSaverWarning = isAffectedByDataSaver();
1233 showOsOptimizationWarning(showBatteryWarning, showDataSaverWarning);
1234 this.binding.sessionEst.setText(
1235 UIHelper.readableTimeDifferenceFull(
1236 this, this.mAccount.getXmppConnection().getLastSessionEstablished()));
1237 if (features.rosterVersioning()) {
1238 this.binding.serverInfoRosterVersion.setText(R.string.server_info_available);
1239 } else {
1240 this.binding.serverInfoRosterVersion.setText(R.string.server_info_unavailable);
1241 }
1242 if (features.carbons()) {
1243 this.binding.serverInfoCarbons.setText(R.string.server_info_available);
1244 } else {
1245 this.binding.serverInfoCarbons.setText(R.string.server_info_unavailable);
1246 }
1247 if (features.mam()) {
1248 this.binding.serverInfoMam.setText(R.string.server_info_available);
1249 } else {
1250 this.binding.serverInfoMam.setText(R.string.server_info_unavailable);
1251 }
1252 if (features.csi()) {
1253 this.binding.serverInfoCsi.setText(R.string.server_info_available);
1254 } else {
1255 this.binding.serverInfoCsi.setText(R.string.server_info_unavailable);
1256 }
1257 if (features.blocking()) {
1258 this.binding.serverInfoBlocking.setText(R.string.server_info_available);
1259 } else {
1260 this.binding.serverInfoBlocking.setText(R.string.server_info_unavailable);
1261 }
1262 if (features.sm()) {
1263 this.binding.serverInfoSm.setText(R.string.server_info_available);
1264 } else {
1265 this.binding.serverInfoSm.setText(R.string.server_info_unavailable);
1266 }
1267 if (features.externalServiceDiscovery()) {
1268 this.binding.serverInfoExternalService.setText(R.string.server_info_available);
1269 } else {
1270 this.binding.serverInfoExternalService.setText(R.string.server_info_unavailable);
1271 }
1272 if (features.bind2()) {
1273 this.binding.serverInfoBind2.setText(R.string.server_info_available);
1274 } else {
1275 this.binding.serverInfoBind2.setText(R.string.server_info_unavailable);
1276 }
1277 if (features.sasl2()) {
1278 this.binding.serverInfoSasl2.setText(R.string.server_info_available);
1279 } else {
1280 this.binding.serverInfoSasl2.setText(R.string.server_info_unavailable);
1281 }
1282 this.binding.loginMechanism.setText(Strings.nullToEmpty(features.loginMechanism()));
1283 if (features.pep()) {
1284 AxolotlService axolotlService = this.mAccount.getAxolotlService();
1285 if (axolotlService != null && axolotlService.isPepBroken()) {
1286 this.binding.serverInfoPep.setText(R.string.server_info_broken);
1287 } else if (features.pepPublishOptions() || features.pepOmemoWhitelisted()) {
1288 this.binding.serverInfoPep.setText(R.string.server_info_available);
1289 } else {
1290 this.binding.serverInfoPep.setText(R.string.server_info_partial);
1291 }
1292 } else {
1293 this.binding.serverInfoPep.setText(R.string.server_info_unavailable);
1294 }
1295 if (features.httpUpload(0)) {
1296 final long maxFileSize = features.getMaxHttpUploadSize();
1297 if (maxFileSize > 0) {
1298 this.binding.serverInfoHttpUpload.setText(
1299 UIHelper.filesizeToString(maxFileSize));
1300 } else {
1301 this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
1302 }
1303 } else {
1304 this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable);
1305 }
1306
1307 this.binding.pushRow.setVisibility(
1308 xmppConnectionService.getPushManagementService().isStub()
1309 ? View.GONE
1310 : View.VISIBLE);
1311
1312 if (xmppConnectionService.getPushManagementService().available(mAccount)) {
1313 this.binding.serverInfoPush.setText(R.string.server_info_available);
1314 } else {
1315 this.binding.serverInfoPush.setText(R.string.server_info_unavailable);
1316 }
1317 final long pgpKeyId = this.mAccount.getPgpId();
1318 if (pgpKeyId != 0 && Config.supportOpenPgp()) {
1319 OnClickListener openPgp = view -> launchOpenKeyChain(pgpKeyId);
1320 OnClickListener delete = view -> showDeletePgpDialog();
1321 this.binding.pgpFingerprintBox.setVisibility(View.VISIBLE);
1322 this.binding.pgpFingerprint.setText(OpenPgpUtils.convertKeyIdToHex(pgpKeyId));
1323 this.binding.pgpFingerprint.setOnClickListener(openPgp);
1324 if ("pgp".equals(messageFingerprint)) {
1325 this.binding.pgpFingerprintDesc.setTextColor(
1326 MaterialColors.getColor(
1327 binding.pgpFingerprintDesc,
1328 com.google.android.material.R.attr.colorPrimaryVariant));
1329 }
1330 this.binding.pgpFingerprintDesc.setOnClickListener(openPgp);
1331 this.binding.actionDeletePgp.setOnClickListener(delete);
1332 } else {
1333 this.binding.pgpFingerprintBox.setVisibility(View.GONE);
1334 }
1335 final String ownAxolotlFingerprint =
1336 this.mAccount.getAxolotlService().getOwnFingerprint();
1337 if (ownAxolotlFingerprint != null && Config.supportOmemo()) {
1338 this.binding.axolotlFingerprintBox.setVisibility(View.VISIBLE);
1339 this.binding.axolotlFingerprintBox.setOnCreateContextMenuListener(
1340 (menu, v, menuInfo) -> {
1341 getMenuInflater().inflate(R.menu.omemo_key_context, menu);
1342 menu.findItem(R.id.verify_scan).setVisible(false);
1343 menu.findItem(R.id.distrust_key).setVisible(false);
1344 this.mSelectedFingerprint = ownAxolotlFingerprint;
1345 });
1346 if (ownAxolotlFingerprint.equals(messageFingerprint)) {
1347 this.binding.ownFingerprintDesc.setTextColor(
1348 MaterialColors.getColor(
1349 binding.ownFingerprintDesc,
1350 com.google.android.material.R.attr.colorPrimaryVariant));
1351 this.binding.ownFingerprintDesc.setText(
1352 R.string.omemo_fingerprint_selected_message);
1353 } else {
1354 this.binding.ownFingerprintDesc.setTextColor(
1355 MaterialColors.getColor(
1356 binding.ownFingerprintDesc,
1357 com.google.android.material.R.attr.colorOnSurface));
1358 this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint);
1359 }
1360 this.binding.axolotlFingerprint.setText(
1361 CryptoHelper.prettifyFingerprint(ownAxolotlFingerprint.substring(2)));
1362 this.binding.showQrCodeButton.setVisibility(View.VISIBLE);
1363 this.binding.showQrCodeButton.setOnClickListener(v -> showQrCode());
1364 } else {
1365 this.binding.axolotlFingerprintBox.setVisibility(View.GONE);
1366 }
1367 boolean hasKeys = false;
1368 boolean showUnverifiedWarning = false;
1369 binding.otherDeviceKeys.removeAllViews();
1370 for (final XmppAxolotlSession session :
1371 mAccount.getAxolotlService().findOwnSessions()) {
1372 final FingerprintStatus trust = session.getTrust();
1373 if (!trust.isCompromised()) {
1374 boolean highlight = session.getFingerprint().equals(messageFingerprint);
1375 addFingerprintRow(binding.otherDeviceKeys, session, highlight);
1376 hasKeys = true;
1377 }
1378 if (trust.isUnverified()) {
1379 showUnverifiedWarning = true;
1380 }
1381 }
1382 if (hasKeys
1383 && Config.supportOmemo()) { // TODO: either the button should be visible if we
1384 // print an active device or the device list should
1385 // be fed with reactivated devices
1386 this.binding.otherDeviceKeysCard.setVisibility(View.VISIBLE);
1387 Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
1388 if (otherDevices == null || otherDevices.isEmpty()) {
1389 binding.clearDevices.setVisibility(View.GONE);
1390 } else {
1391 binding.clearDevices.setVisibility(View.VISIBLE);
1392 }
1393 binding.unverifiedWarning.setVisibility(
1394 showUnverifiedWarning ? View.VISIBLE : View.GONE);
1395 binding.scanButton.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE);
1396 } else {
1397 this.binding.otherDeviceKeysCard.setVisibility(View.GONE);
1398 }
1399 this.binding.serviceOutage.setVisibility(View.GONE);
1400 } else {
1401 final TextInputLayout errorLayout;
1402 final var status = this.mAccount.getStatus();
1403 if (status.isError()
1404 || Arrays.asList(
1405 Account.State.NO_INTERNET,
1406 Account.State.MISSING_INTERNET_PERMISSION)
1407 .contains(status)) {
1408 if (status == Account.State.UNAUTHORIZED
1409 || status == Account.State.DOWNGRADE_ATTACK) {
1410 errorLayout = this.binding.accountPasswordLayout;
1411 } else if (mShowOptions
1412 && status == Account.State.SERVER_NOT_FOUND
1413 && this.binding.hostname.getText().length() > 0) {
1414 errorLayout = this.binding.hostnameLayout;
1415 } else {
1416 errorLayout = this.binding.accountJidLayout;
1417 }
1418 errorLayout.setError(getString(this.mAccount.getStatus().getReadableId()));
1419 if (init || !accountInfoEdited()) {
1420 errorLayout.requestFocus();
1421 }
1422 } else {
1423 errorLayout = null;
1424 }
1425 removeErrorsOnAllBut(errorLayout);
1426 this.binding.stats.setVisibility(View.GONE);
1427 this.binding.otherDeviceKeysCard.setVisibility(View.GONE);
1428 final var sos = mAccount.getServiceOutageStatus();
1429 if (mAccount.isServiceOutage() && sos != null) {
1430 this.binding.serviceOutage.setVisibility(View.VISIBLE);
1431 if (sos.isPlanned()) {
1432 this.binding.sosTitle.setText(R.string.account_status_service_outage_scheduled);
1433 } else {
1434 this.binding.sosTitle.setText(R.string.account_status_service_outage_known);
1435 }
1436 final var sosMessage = sos.getMessage();
1437 if (Strings.isNullOrEmpty(sosMessage)) {
1438 this.binding.sosMessage.setVisibility(View.GONE);
1439 } else {
1440 final var sosMessageSpannable = new SpannableString(sosMessage);
1441 Linkify.addLinks(sosMessageSpannable);
1442 FixedURLSpan.fix(sosMessageSpannable);
1443 this.binding.sosMessage.setText(sosMessageSpannable);
1444 this.binding.sosMessage.setVisibility(View.VISIBLE);
1445 this.binding.sosMessage.setMovementMethod(LinkMovementMethod.getInstance());
1446 }
1447 final var expectedEnd = sos.getExpectedEnd();
1448 if (expectedEnd <= 0) {
1449 this.binding.sosScheduledEnd.setVisibility(View.GONE);
1450 } else {
1451 this.binding.sosScheduledEnd.setVisibility(View.VISIBLE);
1452 this.binding.sosScheduledEnd.setText(
1453 getString(
1454 R.string.sos_scheduled_return,
1455 DateUtils.formatDateTime(
1456 this,
1457 expectedEnd,
1458 DateUtils.FORMAT_SHOW_TIME
1459 | DateUtils.FORMAT_NUMERIC_DATE
1460 | DateUtils.FORMAT_SHOW_YEAR
1461 | DateUtils.FORMAT_SHOW_DATE)));
1462 }
1463 } else {
1464 this.binding.serviceOutage.setVisibility(View.GONE);
1465 }
1466 }
1467 }
1468
1469 private void updateDisplayName(String displayName) {
1470 if (TextUtils.isEmpty(displayName)) {
1471 this.binding.yourName.setText(R.string.no_name_set_instructions);
1472 this.binding.yourName.setTextColor(
1473 MaterialColors.getColor(
1474 binding.yourName,
1475 com.google.android.material.R.attr.colorOnSurfaceVariant));
1476 } else {
1477 this.binding.yourName.setText(displayName);
1478 this.binding.yourName.setTextColor(
1479 MaterialColors.getColor(
1480 binding.yourName,
1481 com.google.android.material.R.attr.colorOnSurfaceVariant));
1482 }
1483 }
1484
1485 private void removeErrorsOnAllBut(TextInputLayout exception) {
1486 if (this.binding.accountJidLayout != exception) {
1487 this.binding.accountJidLayout.setErrorEnabled(false);
1488 this.binding.accountJidLayout.setError(null);
1489 }
1490 if (this.binding.accountPasswordLayout != exception) {
1491 this.binding.accountPasswordLayout.setErrorEnabled(false);
1492 this.binding.accountPasswordLayout.setError(null);
1493 }
1494 if (this.binding.hostnameLayout != exception) {
1495 this.binding.hostnameLayout.setErrorEnabled(false);
1496 this.binding.hostnameLayout.setError(null);
1497 }
1498 if (this.binding.portLayout != exception) {
1499 this.binding.portLayout.setErrorEnabled(false);
1500 this.binding.portLayout.setError(null);
1501 }
1502 }
1503
1504 private void showDeletePgpDialog() {
1505 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1506 builder.setTitle(R.string.unpublish_pgp);
1507 builder.setMessage(R.string.unpublish_pgp_message);
1508 builder.setNegativeButton(R.string.cancel, null);
1509 builder.setPositiveButton(
1510 R.string.confirm,
1511 (dialogInterface, i) -> {
1512 mAccount.setPgpSignId(0);
1513 mAccount.unsetPgpSignature();
1514 xmppConnectionService.databaseBackend.updateAccount(mAccount);
1515 xmppConnectionService.sendPresence(mAccount);
1516 refreshUiReal();
1517 });
1518 builder.create().show();
1519 }
1520
1521 private void showOsOptimizationWarning(
1522 boolean showBatteryWarning, boolean showDataSaverWarning) {
1523 this.binding.osOptimization.setVisibility(
1524 showBatteryWarning || showDataSaverWarning ? View.VISIBLE : View.GONE);
1525 if (showDataSaverWarning
1526 && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
1527 this.binding.osOptimizationHeadline.setText(R.string.data_saver_enabled);
1528 this.binding.osOptimizationBody.setText(
1529 getString(R.string.data_saver_enabled_explained, getString(R.string.app_name)));
1530 this.binding.osOptimizationDisable.setText(R.string.allow);
1531 this.binding.osOptimizationDisable.setOnClickListener(
1532 v -> {
1533 Intent intent =
1534 new Intent(
1535 Settings
1536 .ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS);
1537 Uri uri = Uri.parse("package:" + getPackageName());
1538 intent.setData(uri);
1539 try {
1540 startActivityForResult(intent, REQUEST_DATA_SAVER);
1541 } catch (ActivityNotFoundException e) {
1542 Toast.makeText(
1543 EditAccountActivity.this,
1544 getString(
1545 R.string.device_does_not_support_data_saver,
1546 getString(R.string.app_name)),
1547 Toast.LENGTH_SHORT)
1548 .show();
1549 }
1550 });
1551 } else if (showBatteryWarning) {
1552 this.binding.osOptimizationDisable.setText(R.string.disable);
1553 this.binding.osOptimizationHeadline.setText(R.string.battery_optimizations_enabled);
1554 this.binding.osOptimizationBody.setText(
1555 getString(
1556 R.string.battery_optimizations_enabled_explained,
1557 getString(R.string.app_name)));
1558 this.binding.osOptimizationDisable.setOnClickListener(
1559 v -> {
1560 Intent intent =
1561 new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
1562 Uri uri = Uri.parse("package:" + getPackageName());
1563 intent.setData(uri);
1564 try {
1565 startActivityForResult(intent, REQUEST_BATTERY_OP);
1566 } catch (ActivityNotFoundException e) {
1567 Toast.makeText(
1568 EditAccountActivity.this,
1569 R.string.device_does_not_support_battery_op,
1570 Toast.LENGTH_SHORT)
1571 .show();
1572 }
1573 });
1574 }
1575 }
1576
1577 public void showWipePepDialog() {
1578 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1579 builder.setTitle(getString(R.string.clear_other_devices));
1580 builder.setIconAttribute(android.R.attr.alertDialogIcon);
1581 builder.setMessage(getString(R.string.clear_other_devices_desc));
1582 builder.setNegativeButton(getString(R.string.cancel), null);
1583 builder.setPositiveButton(
1584 getString(R.string.accept),
1585 (dialog, which) -> mAccount.getAxolotlService().wipeOtherPepDevices());
1586 builder.create().show();
1587 }
1588
1589 private void editMamPrefs() {
1590 this.mFetchingMamPrefsToast =
1591 Toast.makeText(this, R.string.fetching_mam_prefs, Toast.LENGTH_LONG);
1592 this.mFetchingMamPrefsToast.show();
1593 xmppConnectionService.fetchMamPreferences(mAccount, this);
1594 }
1595
1596 @Override
1597 public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
1598 refreshUi();
1599 }
1600
1601 @Override
1602 public void onCaptchaRequested(
1603 final Account account, final String id, final Data data, final Bitmap captcha) {
1604 runOnUiThread(
1605 () -> {
1606 if (mCaptchaDialog != null && mCaptchaDialog.isShowing()) {
1607 mCaptchaDialog.dismiss();
1608 }
1609 if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
1610 Log.d(Config.LOGTAG, "activity not running when captcha was requested");
1611 return;
1612 }
1613 final MaterialAlertDialogBuilder builder =
1614 new MaterialAlertDialogBuilder(EditAccountActivity.this);
1615 final View view = getLayoutInflater().inflate(R.layout.captcha, null);
1616 final ImageView imageView = view.findViewById(R.id.captcha);
1617 final EditText input = view.findViewById(R.id.input);
1618 imageView.setImageBitmap(captcha);
1619
1620 builder.setTitle(getString(R.string.captcha_required));
1621 builder.setView(view);
1622
1623 builder.setPositiveButton(
1624 getString(R.string.ok),
1625 (dialog, which) -> {
1626 String rc = input.getText().toString();
1627 data.put("username", account.getUsername());
1628 data.put("password", account.getPassword());
1629 data.put("ocr", rc);
1630 data.submit();
1631
1632 if (xmppConnectionServiceBound) {
1633 xmppConnectionService.sendCreateAccountWithCaptchaPacket(
1634 account, id, data);
1635 }
1636 });
1637 builder.setNegativeButton(
1638 getString(R.string.cancel),
1639 (dialog, which) -> {
1640 if (xmppConnectionService != null) {
1641 xmppConnectionService.sendCreateAccountWithCaptchaPacket(
1642 account, null, null);
1643 }
1644 });
1645
1646 builder.setOnCancelListener(
1647 dialog -> {
1648 if (xmppConnectionService != null) {
1649 xmppConnectionService.sendCreateAccountWithCaptchaPacket(
1650 account, null, null);
1651 }
1652 });
1653 mCaptchaDialog = builder.create();
1654 mCaptchaDialog.show();
1655 input.requestFocus();
1656 });
1657 }
1658
1659 public void onShowErrorToast(final int resId) {
1660 runOnUiThread(
1661 () -> Toast.makeText(EditAccountActivity.this, resId, Toast.LENGTH_SHORT).show());
1662 }
1663
1664 @Override
1665 public void onPreferencesFetched(final Element prefs) {
1666 runOnUiThread(
1667 () -> {
1668 if (mFetchingMamPrefsToast != null) {
1669 mFetchingMamPrefsToast.cancel();
1670 }
1671 final MaterialAlertDialogBuilder builder =
1672 new MaterialAlertDialogBuilder(EditAccountActivity.this);
1673 builder.setTitle(R.string.server_side_mam_prefs);
1674 String defaultAttr = prefs.getAttribute("default");
1675 final List<String> defaults = Arrays.asList("never", "roster", "always");
1676 final AtomicInteger choice =
1677 new AtomicInteger(Math.max(0, defaults.indexOf(defaultAttr)));
1678 builder.setSingleChoiceItems(
1679 R.array.mam_prefs, choice.get(), (dialog, which) -> choice.set(which));
1680 builder.setNegativeButton(R.string.cancel, null);
1681 builder.setPositiveButton(
1682 R.string.ok,
1683 (dialog, which) -> {
1684 prefs.setAttribute("default", defaults.get(choice.get()));
1685 xmppConnectionService.pushMamPreferences(mAccount, prefs);
1686 });
1687 if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
1688 builder.create().show();
1689 }
1690 });
1691 }
1692
1693 @Override
1694 public void onPreferencesFetchFailed() {
1695 runOnUiThread(
1696 () -> {
1697 if (mFetchingMamPrefsToast != null) {
1698 mFetchingMamPrefsToast.cancel();
1699 }
1700 Toast.makeText(
1701 EditAccountActivity.this,
1702 R.string.unable_to_fetch_mam_prefs,
1703 Toast.LENGTH_LONG)
1704 .show();
1705 });
1706 }
1707
1708 @Override
1709 public void OnUpdateBlocklist(Status status) {
1710 refreshUi();
1711 }
1712}