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