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(mAccount, () -> finish());
1049 }
1050
1051 private boolean inNeedOfSaslAccept() {
1052 return mAccount != null
1053 && mAccount.getLastErrorStatus() == Account.State.DOWNGRADE_ATTACK
1054 && mAccount.getPinnedMechanismPriority() >= 0
1055 && !accountInfoEdited();
1056 }
1057
1058 private void shareBarcode() {
1059 Intent intent = new Intent(Intent.ACTION_SEND);
1060 intent.putExtra(Intent.EXTRA_STREAM, BarcodeProvider.getUriForAccount(this, mAccount));
1061 intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1062 intent.setType("image/png");
1063 startActivity(Intent.createChooser(intent, getText(R.string.share_with)));
1064 }
1065
1066 private void changeMoreTableVisibility(final boolean visible) {
1067 binding.serverInfoMore.setVisibility(visible ? View.VISIBLE : View.GONE);
1068 binding.serverInfoLoginMechanism.setVisibility(visible ? View.VISIBLE : View.GONE);
1069 }
1070
1071 private void gotoChangePassword() {
1072 final Intent changePasswordIntent = new Intent(this, ChangePasswordActivity.class);
1073 changePasswordIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toEscapedString());
1074 startActivity(changePasswordIntent);
1075 }
1076
1077 private void renewCertificate() {
1078 KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
1079 }
1080
1081 private void changePresence() {
1082 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1083 boolean manualStatus =
1084 sharedPreferences.getBoolean(
1085 AppSettings.MANUALLY_CHANGE_PRESENCE,
1086 getResources().getBoolean(R.bool.manually_change_presence));
1087 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1088 final DialogPresenceBinding binding =
1089 DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_presence, null, false);
1090 String current = mAccount.getPresenceStatusMessage();
1091 if (current != null && !current.trim().isEmpty()) {
1092 binding.statusMessage.append(current);
1093 }
1094 setAvailabilityRadioButton(mAccount.getPresenceStatus(), binding);
1095 binding.show.setVisibility(manualStatus ? View.VISIBLE : View.GONE);
1096 List<PresenceTemplate> templates = xmppConnectionService.getPresenceTemplates(mAccount);
1097 PresenceTemplateAdapter presenceTemplateAdapter =
1098 new PresenceTemplateAdapter(this, R.layout.item_autocomplete, templates);
1099 binding.statusMessage.setAdapter(presenceTemplateAdapter);
1100 binding.statusMessage.setOnItemClickListener(
1101 (parent, view, position, id) -> {
1102 PresenceTemplate template =
1103 (PresenceTemplate) parent.getItemAtPosition(position);
1104 setAvailabilityRadioButton(template.getStatus(), binding);
1105 });
1106 builder.setTitle(R.string.edit_status_message_title);
1107 builder.setView(binding.getRoot());
1108 builder.setNegativeButton(R.string.cancel, null);
1109 builder.setPositiveButton(
1110 R.string.confirm,
1111 (dialog, which) -> {
1112 PresenceTemplate template =
1113 new PresenceTemplate(
1114 getAvailabilityRadioButton(binding),
1115 binding.statusMessage.getText().toString().trim());
1116 if (mAccount.getPgpId() != 0 && hasPgp()) {
1117 generateSignature(null, template);
1118 } else {
1119 xmppConnectionService.changeStatus(mAccount, template, null);
1120 }
1121 });
1122 builder.create().show();
1123 }
1124
1125 private void generateSignature(Intent intent, PresenceTemplate template) {
1126 xmppConnectionService
1127 .getPgpEngine()
1128 .generateSignature(
1129 intent,
1130 mAccount,
1131 template.getStatusMessage(),
1132 new UiCallback<String>() {
1133 @Override
1134 public void success(String signature) {
1135 xmppConnectionService.changeStatus(mAccount, template, signature);
1136 }
1137
1138 @Override
1139 public void error(int errorCode, String object) {}
1140
1141 @Override
1142 public void userInputRequired(PendingIntent pi, String object) {
1143 mPendingPresenceTemplate.push(template);
1144 try {
1145 startIntentSenderForResult(
1146 pi.getIntentSender(),
1147 REQUEST_CHANGE_STATUS,
1148 null,
1149 0,
1150 0,
1151 0,
1152 Compatibility.pgpStartIntentSenderOptions());
1153 } catch (final IntentSender.SendIntentException ignored) {
1154 }
1155 }
1156 });
1157 }
1158
1159 @Override
1160 public void alias(String alias) {
1161 if (alias != null) {
1162 xmppConnectionService.updateKeyInAccount(mAccount, alias);
1163 }
1164 }
1165
1166 private void updateAccountInformation(boolean init) {
1167 if (init) {
1168 this.binding.accountJid.getEditableText().clear();
1169 if (mUsernameMode) {
1170 this.binding
1171 .accountJid
1172 .getEditableText()
1173 .append(this.mAccount.getJid().getEscapedLocal());
1174 } else {
1175 this.binding
1176 .accountJid
1177 .getEditableText()
1178 .append(this.mAccount.getJid().asBareJid().toEscapedString());
1179 }
1180 this.binding.accountPassword.getEditableText().clear();
1181 this.binding.accountPassword.getEditableText().append(this.mAccount.getPassword());
1182 this.binding.hostname.setText("");
1183 this.binding.hostname.getEditableText().append(this.mAccount.getHostname());
1184 this.binding.port.setText("");
1185 this.binding.port.getEditableText().append(String.valueOf(this.mAccount.getPort()));
1186 this.binding.namePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE);
1187 }
1188
1189 if (!mInitMode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1190 this.binding.accountPassword.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO);
1191 }
1192
1193 final boolean editable =
1194 !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)
1195 && !mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME)
1196 && QuickConversationsService.isConversations();
1197 this.binding.accountJid.setEnabled(editable);
1198 this.binding.accountJid.setFocusable(editable);
1199 this.binding.accountJid.setFocusableInTouchMode(editable);
1200 this.binding.accountJid.setCursorVisible(editable);
1201
1202 final String displayName = mAccount.getDisplayName();
1203 updateDisplayName(displayName);
1204
1205 final boolean togglePassword =
1206 mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)
1207 || !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
1208 final boolean neverLoggedIn =
1209 !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)
1210 && QuickConversationsService.isConversations();
1211 final boolean editPassword = mAccount.unauthorized() || neverLoggedIn;
1212
1213 this.binding.accountPasswordLayout.setPasswordVisibilityToggleEnabled(togglePassword);
1214
1215 this.binding.accountPassword.setFocusable(editPassword);
1216 this.binding.accountPassword.setFocusableInTouchMode(editPassword);
1217 this.binding.accountPassword.setCursorVisible(editPassword);
1218 this.binding.accountPassword.setEnabled(editPassword);
1219
1220 if (!mInitMode) {
1221 this.binding.avater.setVisibility(View.VISIBLE);
1222 AvatarWorkerTask.loadAvatar(
1223 mAccount, binding.avater, R.dimen.avatar_on_details_screen_size);
1224 } else {
1225 this.binding.avater.setVisibility(View.GONE);
1226 }
1227 this.binding.accountRegisterNew.setChecked(
1228 this.mAccount.isOptionSet(Account.OPTION_REGISTER));
1229 if (this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) {
1230 if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) {
1231 ActionBar actionBar = getSupportActionBar();
1232 if (actionBar != null) {
1233 actionBar.setTitle(R.string.create_account);
1234 }
1235 }
1236 this.binding.accountRegisterNew.setVisibility(View.GONE);
1237 } else if (this.mAccount.isOptionSet(Account.OPTION_REGISTER) && mForceRegister == null) {
1238 this.binding.accountRegisterNew.setVisibility(View.VISIBLE);
1239 } else {
1240 this.binding.accountRegisterNew.setVisibility(View.GONE);
1241 }
1242 if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) {
1243 final Features features = this.mAccount.getXmppConnection().getFeatures();
1244 this.binding.stats.setVisibility(View.VISIBLE);
1245 boolean showBatteryWarning = isOptimizingBattery();
1246 boolean showDataSaverWarning = isAffectedByDataSaver();
1247 showOsOptimizationWarning(showBatteryWarning, showDataSaverWarning);
1248 this.binding.sessionEst.setText(
1249 UIHelper.readableTimeDifferenceFull(
1250 this, this.mAccount.getXmppConnection().getLastSessionEstablished()));
1251 if (features.rosterVersioning()) {
1252 this.binding.serverInfoRosterVersion.setText(R.string.server_info_available);
1253 } else {
1254 this.binding.serverInfoRosterVersion.setText(R.string.server_info_unavailable);
1255 }
1256 if (features.carbons()) {
1257 this.binding.serverInfoCarbons.setText(R.string.server_info_available);
1258 } else {
1259 this.binding.serverInfoCarbons.setText(R.string.server_info_unavailable);
1260 }
1261 if (features.mam()) {
1262 this.binding.serverInfoMam.setText(R.string.server_info_available);
1263 } else {
1264 this.binding.serverInfoMam.setText(R.string.server_info_unavailable);
1265 }
1266 if (features.csi()) {
1267 this.binding.serverInfoCsi.setText(R.string.server_info_available);
1268 } else {
1269 this.binding.serverInfoCsi.setText(R.string.server_info_unavailable);
1270 }
1271 if (features.blocking()) {
1272 this.binding.serverInfoBlocking.setText(R.string.server_info_available);
1273 } else {
1274 this.binding.serverInfoBlocking.setText(R.string.server_info_unavailable);
1275 }
1276 if (features.sm()) {
1277 this.binding.serverInfoSm.setText(R.string.server_info_available);
1278 } else {
1279 this.binding.serverInfoSm.setText(R.string.server_info_unavailable);
1280 }
1281 if (features.externalServiceDiscovery()) {
1282 this.binding.serverInfoExternalService.setText(R.string.server_info_available);
1283 } else {
1284 this.binding.serverInfoExternalService.setText(R.string.server_info_unavailable);
1285 }
1286 if (features.bind2()) {
1287 this.binding.serverInfoBind2.setText(R.string.server_info_available);
1288 } else {
1289 this.binding.serverInfoBind2.setText(R.string.server_info_unavailable);
1290 }
1291 if (features.sasl2()) {
1292 this.binding.serverInfoSasl2.setText(R.string.server_info_available);
1293 } else {
1294 this.binding.serverInfoSasl2.setText(R.string.server_info_unavailable);
1295 }
1296 this.binding.loginMechanism.setText(Strings.nullToEmpty(features.loginMechanism()));
1297 if (features.pep()) {
1298 AxolotlService axolotlService = this.mAccount.getAxolotlService();
1299 if (axolotlService != null && axolotlService.isPepBroken()) {
1300 this.binding.serverInfoPep.setText(R.string.server_info_broken);
1301 } else if (features.pepPublishOptions() || features.pepOmemoWhitelisted()) {
1302 this.binding.serverInfoPep.setText(R.string.server_info_available);
1303 } else {
1304 this.binding.serverInfoPep.setText(R.string.server_info_partial);
1305 }
1306 } else {
1307 this.binding.serverInfoPep.setText(R.string.server_info_unavailable);
1308 }
1309 if (features.httpUpload(0)) {
1310 final long maxFileSize = features.getMaxHttpUploadSize();
1311 if (maxFileSize > 0) {
1312 this.binding.serverInfoHttpUpload.setText(
1313 UIHelper.filesizeToString(maxFileSize));
1314 } else {
1315 this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
1316 }
1317 } else {
1318 this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable);
1319 }
1320
1321 this.binding.pushRow.setVisibility(
1322 xmppConnectionService.getPushManagementService().isStub()
1323 ? View.GONE
1324 : View.VISIBLE);
1325
1326 if (xmppConnectionService.getPushManagementService().available(mAccount)) {
1327 this.binding.serverInfoPush.setText(R.string.server_info_available);
1328 } else {
1329 this.binding.serverInfoPush.setText(R.string.server_info_unavailable);
1330 }
1331 final long pgpKeyId = this.mAccount.getPgpId();
1332 if (pgpKeyId != 0 && Config.supportOpenPgp()) {
1333 OnClickListener openPgp = view -> launchOpenKeyChain(pgpKeyId);
1334 OnClickListener delete = view -> showDeletePgpDialog();
1335 this.binding.pgpFingerprintBox.setVisibility(View.VISIBLE);
1336 this.binding.pgpFingerprint.setText(OpenPgpUtils.convertKeyIdToHex(pgpKeyId));
1337 this.binding.pgpFingerprint.setOnClickListener(openPgp);
1338 if ("pgp".equals(messageFingerprint)) {
1339 this.binding.pgpFingerprintDesc.setTextColor(
1340 MaterialColors.getColor(
1341 binding.pgpFingerprintDesc,
1342 com.google.android.material.R.attr.colorPrimaryVariant));
1343 }
1344 this.binding.pgpFingerprintDesc.setOnClickListener(openPgp);
1345 this.binding.actionDeletePgp.setOnClickListener(delete);
1346 } else {
1347 this.binding.pgpFingerprintBox.setVisibility(View.GONE);
1348 }
1349 final String ownAxolotlFingerprint =
1350 this.mAccount.getAxolotlService().getOwnFingerprint();
1351 if (ownAxolotlFingerprint != null && Config.supportOmemo()) {
1352 this.binding.axolotlFingerprintBox.setVisibility(View.VISIBLE);
1353 if (ownAxolotlFingerprint.equals(messageFingerprint)) {
1354 this.binding.ownFingerprintDesc.setTextColor(
1355 MaterialColors.getColor(
1356 binding.ownFingerprintDesc,
1357 com.google.android.material.R.attr.colorPrimaryVariant));
1358 this.binding.ownFingerprintDesc.setText(
1359 R.string.omemo_fingerprint_selected_message);
1360 } else {
1361 this.binding.ownFingerprintDesc.setTextColor(
1362 MaterialColors.getColor(
1363 binding.ownFingerprintDesc,
1364 com.google.android.material.R.attr.colorOnSurface));
1365 this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint);
1366 }
1367 this.binding.axolotlFingerprint.setText(
1368 CryptoHelper.prettifyFingerprint(ownAxolotlFingerprint.substring(2)));
1369 this.binding.showQrCodeButton.setVisibility(View.VISIBLE);
1370 this.binding.showQrCodeButton.setOnClickListener(v -> showQrCode());
1371 } else {
1372 this.binding.axolotlFingerprintBox.setVisibility(View.GONE);
1373 }
1374 boolean hasKeys = false;
1375 boolean showUnverifiedWarning = false;
1376 binding.otherDeviceKeys.removeAllViews();
1377 for (final XmppAxolotlSession session :
1378 mAccount.getAxolotlService().findOwnSessions()) {
1379 final FingerprintStatus trust = session.getTrust();
1380 if (!trust.isCompromised()) {
1381 boolean highlight = session.getFingerprint().equals(messageFingerprint);
1382 addFingerprintRow(binding.otherDeviceKeys, session, highlight);
1383 hasKeys = true;
1384 }
1385 if (trust.isUnverified()) {
1386 showUnverifiedWarning = true;
1387 }
1388 }
1389 if (hasKeys
1390 && Config.supportOmemo()) { // TODO: either the button should be visible if we
1391 // print an active device or the device list should
1392 // be fed with reactivated devices
1393 this.binding.otherDeviceKeysCard.setVisibility(View.VISIBLE);
1394 Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
1395 if (otherDevices == null || otherDevices.isEmpty()) {
1396 binding.clearDevices.setVisibility(View.GONE);
1397 } else {
1398 binding.clearDevices.setVisibility(View.VISIBLE);
1399 }
1400 binding.unverifiedWarning.setVisibility(
1401 showUnverifiedWarning ? View.VISIBLE : View.GONE);
1402 binding.scanButton.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE);
1403 } else {
1404 this.binding.otherDeviceKeysCard.setVisibility(View.GONE);
1405 }
1406 } else {
1407 final TextInputLayout errorLayout;
1408 final var status = this.mAccount.getStatus();
1409 if (status.isError()
1410 || Arrays.asList(
1411 Account.State.NO_INTERNET,
1412 Account.State.MISSING_INTERNET_PERMISSION)
1413 .contains(status)) {
1414 if (status == Account.State.UNAUTHORIZED
1415 || status == Account.State.DOWNGRADE_ATTACK) {
1416 errorLayout = this.binding.accountPasswordLayout;
1417 } else if (mShowOptions
1418 && status == Account.State.SERVER_NOT_FOUND
1419 && this.binding.hostname.getText().length() > 0) {
1420 errorLayout = this.binding.hostnameLayout;
1421 } else {
1422 errorLayout = this.binding.accountJidLayout;
1423 }
1424 errorLayout.setError(getString(this.mAccount.getStatus().getReadableId()));
1425 if (init || !accountInfoEdited()) {
1426 errorLayout.requestFocus();
1427 }
1428 } else {
1429 errorLayout = null;
1430 }
1431 removeErrorsOnAllBut(errorLayout);
1432 this.binding.stats.setVisibility(View.GONE);
1433 this.binding.otherDeviceKeysCard.setVisibility(View.GONE);
1434 }
1435 }
1436
1437 private void updateDisplayName(String displayName) {
1438 if (TextUtils.isEmpty(displayName)) {
1439 this.binding.yourName.setText(R.string.no_name_set_instructions);
1440 this.binding.yourName.setTextColor(
1441 MaterialColors.getColor(
1442 binding.yourName,
1443 com.google.android.material.R.attr.colorOnSurfaceVariant));
1444 } else {
1445 this.binding.yourName.setText(displayName);
1446 this.binding.yourName.setTextColor(
1447 MaterialColors.getColor(
1448 binding.yourName,
1449 com.google.android.material.R.attr.colorOnSurfaceVariant));
1450 }
1451 }
1452
1453 private void removeErrorsOnAllBut(TextInputLayout exception) {
1454 if (this.binding.accountJidLayout != exception) {
1455 this.binding.accountJidLayout.setErrorEnabled(false);
1456 this.binding.accountJidLayout.setError(null);
1457 }
1458 if (this.binding.accountPasswordLayout != exception) {
1459 this.binding.accountPasswordLayout.setErrorEnabled(false);
1460 this.binding.accountPasswordLayout.setError(null);
1461 }
1462 if (this.binding.hostnameLayout != exception) {
1463 this.binding.hostnameLayout.setErrorEnabled(false);
1464 this.binding.hostnameLayout.setError(null);
1465 }
1466 if (this.binding.portLayout != exception) {
1467 this.binding.portLayout.setErrorEnabled(false);
1468 this.binding.portLayout.setError(null);
1469 }
1470 }
1471
1472 private void showDeletePgpDialog() {
1473 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1474 builder.setTitle(R.string.unpublish_pgp);
1475 builder.setMessage(R.string.unpublish_pgp_message);
1476 builder.setNegativeButton(R.string.cancel, null);
1477 builder.setPositiveButton(
1478 R.string.confirm,
1479 (dialogInterface, i) -> {
1480 mAccount.setPgpSignId(0);
1481 mAccount.unsetPgpSignature();
1482 xmppConnectionService.databaseBackend.updateAccount(mAccount);
1483 xmppConnectionService.sendPresence(mAccount);
1484 refreshUiReal();
1485 });
1486 builder.create().show();
1487 }
1488
1489 private void showOsOptimizationWarning(
1490 boolean showBatteryWarning, boolean showDataSaverWarning) {
1491 this.binding.osOptimization.setVisibility(
1492 showBatteryWarning || showDataSaverWarning ? View.VISIBLE : View.GONE);
1493 if (showDataSaverWarning
1494 && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
1495 this.binding.osOptimizationHeadline.setText(R.string.data_saver_enabled);
1496 this.binding.osOptimizationBody.setText(
1497 getString(R.string.data_saver_enabled_explained, getString(R.string.app_name)));
1498 this.binding.osOptimizationDisable.setText(R.string.allow);
1499 this.binding.osOptimizationDisable.setOnClickListener(
1500 v -> {
1501 Intent intent =
1502 new Intent(
1503 Settings
1504 .ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS);
1505 Uri uri = Uri.parse("package:" + getPackageName());
1506 intent.setData(uri);
1507 try {
1508 startActivityForResult(intent, REQUEST_DATA_SAVER);
1509 } catch (ActivityNotFoundException e) {
1510 Toast.makeText(
1511 EditAccountActivity.this,
1512 getString(
1513 R.string.device_does_not_support_data_saver,
1514 getString(R.string.app_name)),
1515 Toast.LENGTH_SHORT)
1516 .show();
1517 }
1518 });
1519 } else if (showBatteryWarning) {
1520 this.binding.osOptimizationDisable.setText(R.string.disable);
1521 this.binding.osOptimizationHeadline.setText(R.string.battery_optimizations_enabled);
1522 this.binding.osOptimizationBody.setText(
1523 getString(
1524 R.string.battery_optimizations_enabled_explained,
1525 getString(R.string.app_name)));
1526 this.binding.osOptimizationDisable.setOnClickListener(
1527 v -> {
1528 Intent intent =
1529 new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
1530 Uri uri = Uri.parse("package:" + getPackageName());
1531 intent.setData(uri);
1532 try {
1533 startActivityForResult(intent, REQUEST_BATTERY_OP);
1534 } catch (ActivityNotFoundException e) {
1535 Toast.makeText(
1536 EditAccountActivity.this,
1537 R.string.device_does_not_support_battery_op,
1538 Toast.LENGTH_SHORT)
1539 .show();
1540 }
1541 });
1542 }
1543 }
1544
1545 public void showWipePepDialog() {
1546 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1547 builder.setTitle(getString(R.string.clear_other_devices));
1548 builder.setIconAttribute(android.R.attr.alertDialogIcon);
1549 builder.setMessage(getString(R.string.clear_other_devices_desc));
1550 builder.setNegativeButton(getString(R.string.cancel), null);
1551 builder.setPositiveButton(
1552 getString(R.string.accept),
1553 (dialog, which) -> mAccount.getAxolotlService().wipeOtherPepDevices());
1554 builder.create().show();
1555 }
1556
1557 private void editMamPrefs() {
1558 this.mFetchingMamPrefsToast =
1559 Toast.makeText(this, R.string.fetching_mam_prefs, Toast.LENGTH_LONG);
1560 this.mFetchingMamPrefsToast.show();
1561 xmppConnectionService.fetchMamPreferences(mAccount, this);
1562 }
1563
1564 @Override
1565 public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
1566 refreshUi();
1567 }
1568
1569 @Override
1570 public void onCaptchaRequested(
1571 final Account account, final String id, final Data data, final Bitmap captcha) {
1572 runOnUiThread(
1573 () -> {
1574 if (mCaptchaDialog != null && mCaptchaDialog.isShowing()) {
1575 mCaptchaDialog.dismiss();
1576 }
1577 if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
1578 Log.d(Config.LOGTAG, "activity not running when captcha was requested");
1579 return;
1580 }
1581 final MaterialAlertDialogBuilder builder =
1582 new MaterialAlertDialogBuilder(EditAccountActivity.this);
1583 final View view = getLayoutInflater().inflate(R.layout.captcha, null);
1584 final ImageView imageView = view.findViewById(R.id.captcha);
1585 final EditText input = view.findViewById(R.id.input);
1586 imageView.setImageBitmap(captcha);
1587
1588 builder.setTitle(getString(R.string.captcha_required));
1589 builder.setView(view);
1590
1591 builder.setPositiveButton(
1592 getString(R.string.ok),
1593 (dialog, which) -> {
1594 String rc = input.getText().toString();
1595 data.put("username", account.getUsername());
1596 data.put("password", account.getPassword());
1597 data.put("ocr", rc);
1598 data.submit();
1599
1600 if (xmppConnectionServiceBound) {
1601 xmppConnectionService.sendCreateAccountWithCaptchaPacket(
1602 account, id, data);
1603 }
1604 });
1605 builder.setNegativeButton(
1606 getString(R.string.cancel),
1607 (dialog, which) -> {
1608 if (xmppConnectionService != null) {
1609 xmppConnectionService.sendCreateAccountWithCaptchaPacket(
1610 account, null, null);
1611 }
1612 });
1613
1614 builder.setOnCancelListener(
1615 dialog -> {
1616 if (xmppConnectionService != null) {
1617 xmppConnectionService.sendCreateAccountWithCaptchaPacket(
1618 account, null, null);
1619 }
1620 });
1621 mCaptchaDialog = builder.create();
1622 mCaptchaDialog.show();
1623 input.requestFocus();
1624 });
1625 }
1626
1627 public void onShowErrorToast(final int resId) {
1628 runOnUiThread(
1629 () -> Toast.makeText(EditAccountActivity.this, resId, Toast.LENGTH_SHORT).show());
1630 }
1631
1632 @Override
1633 public void onPreferencesFetched(final Element prefs) {
1634 runOnUiThread(
1635 () -> {
1636 if (mFetchingMamPrefsToast != null) {
1637 mFetchingMamPrefsToast.cancel();
1638 }
1639 final MaterialAlertDialogBuilder builder =
1640 new MaterialAlertDialogBuilder(EditAccountActivity.this);
1641 builder.setTitle(R.string.server_side_mam_prefs);
1642 String defaultAttr = prefs.getAttribute("default");
1643 final List<String> defaults = Arrays.asList("never", "roster", "always");
1644 final AtomicInteger choice =
1645 new AtomicInteger(Math.max(0, defaults.indexOf(defaultAttr)));
1646 builder.setSingleChoiceItems(
1647 R.array.mam_prefs, choice.get(), (dialog, which) -> choice.set(which));
1648 builder.setNegativeButton(R.string.cancel, null);
1649 builder.setPositiveButton(
1650 R.string.ok,
1651 (dialog, which) -> {
1652 prefs.setAttribute("default", defaults.get(choice.get()));
1653 xmppConnectionService.pushMamPreferences(mAccount, prefs);
1654 });
1655 if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
1656 builder.create().show();
1657 }
1658 });
1659 }
1660
1661 @Override
1662 public void onPreferencesFetchFailed() {
1663 runOnUiThread(
1664 () -> {
1665 if (mFetchingMamPrefsToast != null) {
1666 mFetchingMamPrefsToast.cancel();
1667 }
1668 Toast.makeText(
1669 EditAccountActivity.this,
1670 R.string.unable_to_fetch_mam_prefs,
1671 Toast.LENGTH_LONG)
1672 .show();
1673 });
1674 }
1675
1676 @Override
1677 public void OnUpdateBlocklist(Status status) {
1678 refreshUi();
1679 }
1680}