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