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