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.PresenceManager;
86import eu.siacs.conversations.xmpp.manager.RegistrationManager;
87import im.conversations.android.xmpp.model.data.Data;
88import im.conversations.android.xmpp.model.stanza.Presence;
89import java.util.Arrays;
90import java.util.List;
91import java.util.Set;
92import java.util.concurrent.atomic.AtomicInteger;
93import okhttp3.HttpUrl;
94import org.openintents.openpgp.util.OpenPgpUtils;
95
96public class EditAccountActivity extends OmemoActivity
97 implements OnAccountUpdate,
98 OnUpdateBlocklist,
99 OnKeyStatusUpdated,
100 OnCaptchaRequested,
101 KeyChainAliasCallback,
102 XmppConnectionService.OnShowErrorToast,
103 XmppConnectionService.OnMamPreferencesFetched {
104
105 public static final String EXTRA_OPENED_FROM_NOTIFICATION = "opened_from_notification";
106 public static final String EXTRA_FORCE_REGISTER = "force_register";
107
108 private static final int REQUEST_DATA_SAVER = 0xf244;
109 private static final int REQUEST_CHANGE_STATUS = 0xee11;
110 private static final int REQUEST_ORBOT = 0xff22;
111 private final PendingItem<PresenceTemplate> mPendingPresenceTemplate = new PendingItem<>();
112 private AlertDialog mCaptchaDialog = null;
113 private Jid jidToEdit;
114 private boolean mInitMode = false;
115 private Boolean mForceRegister = null;
116 private boolean mUsernameMode = false;
117 private boolean mShowOptions = false;
118 private Account mAccount;
119 private final OnClickListener mCancelButtonClickListener =
120 v -> {
121 deleteAccountAndReturnIfNecessary();
122 finish();
123 };
124
125 private final FutureCallback<Void> mAvatarFetchCallback =
126 new FutureCallback<>() {
127 @Override
128 public void onSuccess(Void result) {
129 Log.d(Config.LOGTAG, "found pre-existing avatar");
130 finishInitialSetup(true);
131 }
132
133 @Override
134 public void onFailure(@NonNull Throwable t) {
135 Log.d(Config.LOGTAG, "failed to fetch avatar", t);
136 finishInitialSetup(false);
137 }
138 };
139 private final OnClickListener mAvatarClickListener =
140 new OnClickListener() {
141 @Override
142 public void onClick(final View view) {
143 if (mAccount != null) {
144 final Intent intent =
145 new Intent(
146 getApplicationContext(),
147 PublishProfilePictureActivity.class);
148 intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toString());
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.of(
263 binding.accountJid.getText().toString(),
264 getUserModeDomain(),
265 null);
266 } else {
267 jid = Jid.ofUserInput(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.Availability 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.Availability getAvailabilityRadioButton(DialogPresenceBinding binding) {
438 if (binding.dnd.isChecked()) {
439 return Presence.Availability.DND;
440 } else if (binding.xa.isChecked()) {
441 return Presence.Availability.XA;
442 } else if (binding.away.isChecked()) {
443 return Presence.Availability.AWAY;
444 } else {
445 return Presence.Availability.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 final var future = xmppConnectionService.checkForAvatar(mAccount);
460 Futures.addCallback(future, mAvatarFetchCallback, MoreExecutors.directExecutor());
461 }
462 }
463 if (mAccount != null) {
464 updateAccountInformation(false);
465 }
466 updateSaveButton();
467 }
468
469 @Override
470 public boolean onNavigateUp() {
471 deleteAccountAndReturnIfNecessary();
472 return super.onNavigateUp();
473 }
474
475 @Override
476 public void onBackPressed() {
477 deleteAccountAndReturnIfNecessary();
478 super.onBackPressed();
479 }
480
481 private void deleteAccountAndReturnIfNecessary() {
482 if (mInitMode
483 && mAccount != null
484 && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)) {
485 xmppConnectionService.deleteAccount(mAccount);
486 }
487
488 final boolean magicCreate =
489 mAccount != null
490 && mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)
491 && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
492 final Jid jid = mAccount == null ? null : mAccount.getJid();
493
494 if (SignupUtils.isSupportTokenRegistry()
495 && jid != null
496 && magicCreate
497 && !jid.getDomain().equals(Config.MAGIC_CREATE_DOMAIN)) {
498 final Jid preset;
499 if (mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME)) {
500 preset = jid.asBareJid();
501 } else {
502 preset = jid.getDomain();
503 }
504 final Intent intent =
505 SignupUtils.getTokenRegistrationIntent(
506 this, preset, mAccount.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN));
507 StartConversationActivity.addInviteUri(intent, getIntent());
508 startActivity(intent);
509 return;
510 }
511
512 final List<Account> accounts =
513 xmppConnectionService == null ? null : xmppConnectionService.getAccounts();
514 if (accounts != null && accounts.isEmpty() && Config.MAGIC_CREATE_DOMAIN != null) {
515 Intent intent =
516 SignupUtils.getSignUpIntent(this, mForceRegister != null && mForceRegister);
517 StartConversationActivity.addInviteUri(intent, getIntent());
518 startActivity(intent);
519 }
520 }
521
522 @Override
523 public void onAccountUpdate() {
524 refreshUi();
525 }
526
527 protected void finishInitialSetup(final boolean avatar) {
528 runOnUiThread(
529 () -> {
530 SoftKeyboardUtils.hideSoftKeyboard(EditAccountActivity.this);
531 final Intent intent;
532 final XmppConnection connection = mAccount.getXmppConnection();
533 final boolean wasFirstAccount =
534 xmppConnectionService != null
535 && xmppConnectionService.getAccounts().size() == 1;
536 if (avatar || (connection != null && !connection.getFeatures().pep())) {
537 intent =
538 new Intent(
539 getApplicationContext(), StartConversationActivity.class);
540 if (wasFirstAccount) {
541 intent.putExtra("init", true);
542 }
543 intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toString());
544 } else {
545 intent =
546 new Intent(
547 getApplicationContext(),
548 PublishProfilePictureActivity.class);
549 intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toString());
550 intent.putExtra("setup", true);
551 }
552 if (wasFirstAccount) {
553 intent.setFlags(
554 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
555 }
556 StartConversationActivity.addInviteUri(intent, getIntent());
557 startActivity(intent);
558 finish();
559 });
560 }
561
562 @Override
563 public void onActivityResult(int requestCode, int resultCode, Intent data) {
564 // TODO check for Camera / Scan permission
565 super.onActivityResult(requestCode, resultCode, data);
566 if (requestCode == REQUEST_BATTERY_OP || requestCode == REQUEST_DATA_SAVER) {
567 updateAccountInformation(mAccount == null);
568 }
569 if (requestCode == REQUEST_BATTERY_OP) {
570 // the result code is always 0 even when battery permission were granted
571 XmppConnectionService.toggleForegroundService(xmppConnectionService);
572 }
573 if (requestCode == REQUEST_CHANGE_STATUS) {
574 PresenceTemplate template = mPendingPresenceTemplate.pop();
575 if (template != null && resultCode == Activity.RESULT_OK) {
576 generateSignature(data, template);
577 } else {
578 Log.d(Config.LOGTAG, "pgp result not ok");
579 }
580 }
581 }
582
583 @Override
584 protected void processFingerprintVerification(XmppUri uri) {
585 processFingerprintVerification(uri, true);
586 }
587
588 protected void processFingerprintVerification(XmppUri uri, boolean showWarningToast) {
589 if (mAccount != null
590 && mAccount.getJid().asBareJid().equals(uri.getJid())
591 && uri.hasFingerprints()) {
592 if (xmppConnectionService.verifyFingerprints(mAccount, uri.getFingerprints())) {
593 Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
594 updateAccountInformation(false);
595 }
596 } else if (showWarningToast) {
597 Toast.makeText(this, R.string.invalid_barcode, Toast.LENGTH_SHORT).show();
598 }
599 }
600
601 private void updatePortLayout() {
602 final String hostname = this.binding.hostname.getText().toString();
603 if (TextUtils.isEmpty(hostname)) {
604 this.binding.portLayout.setEnabled(false);
605 this.binding.portLayout.setError(null);
606 } else {
607 this.binding.portLayout.setEnabled(true);
608 }
609 }
610
611 protected void updateSaveButton() {
612 boolean accountInfoEdited = accountInfoEdited();
613
614 if (accountInfoEdited && !mInitMode) {
615 this.binding.saveButton.setText(R.string.save);
616 this.binding.saveButton.setEnabled(true);
617 } else if (mAccount != null
618 && (mAccount.getStatus() == Account.State.CONNECTING
619 || mAccount.getStatus() == Account.State.REGISTRATION_SUCCESSFUL
620 || mFetchingAvatar)) {
621 this.binding.saveButton.setEnabled(false);
622 this.binding.saveButton.setText(R.string.account_status_connecting);
623 } else if (mAccount != null
624 && mAccount.getStatus() == Account.State.DISABLED
625 && !mInitMode) {
626 this.binding.saveButton.setEnabled(true);
627 this.binding.saveButton.setText(R.string.enable);
628 } else if (torNeedsInstall(mAccount)) {
629 this.binding.saveButton.setEnabled(true);
630 this.binding.saveButton.setText(R.string.install_orbot);
631 } else if (torNeedsStart(mAccount)) {
632 this.binding.saveButton.setEnabled(true);
633 this.binding.saveButton.setText(R.string.start_orbot);
634 } else {
635 this.binding.saveButton.setEnabled(true);
636 if (!mInitMode) {
637 if (mAccount != null && mAccount.isOnlineAndConnected()) {
638 this.binding.saveButton.setText(R.string.save);
639 if (!accountInfoEdited) {
640 this.binding.saveButton.setEnabled(false);
641 }
642 } else {
643 XmppConnection connection =
644 mAccount == null ? null : mAccount.getXmppConnection();
645 HttpUrl url =
646 connection != null
647 && mAccount.getStatus()
648 == Account.State.PAYMENT_REQUIRED
649 ? connection.getRedirectionUrl()
650 : null;
651 if (url != null) {
652 this.binding.saveButton.setText(R.string.open_website);
653 } else if (inNeedOfSaslAccept()) {
654 this.binding.saveButton.setText(R.string.accept);
655 } else {
656 this.binding.saveButton.setText(R.string.connect);
657 }
658 }
659 } else {
660 XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection();
661 HttpUrl url =
662 connection != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB
663 ? connection.getRedirectionUrl()
664 : null;
665 if (url != null
666 && this.binding.accountRegisterNew.isChecked()
667 && !accountInfoEdited) {
668 this.binding.saveButton.setText(R.string.open_website);
669 } else {
670 this.binding.saveButton.setText(R.string.next);
671 }
672 }
673 }
674 }
675
676 private boolean torNeedsInstall(final Account account) {
677 return account != null
678 && account.getStatus() == Account.State.TOR_NOT_AVAILABLE
679 && !TorServiceUtils.isOrbotInstalled(this);
680 }
681
682 private boolean torNeedsStart(final Account account) {
683 return account != null && account.getStatus() == Account.State.TOR_NOT_AVAILABLE;
684 }
685
686 protected boolean accountInfoEdited() {
687 if (this.mAccount == null) {
688 return false;
689 }
690 return jidEdited()
691 || !this.mAccount
692 .getPassword()
693 .equals(this.binding.accountPassword.getText().toString())
694 || !this.mAccount.getHostname().equals(this.binding.hostname.getText().toString())
695 || !String.valueOf(this.mAccount.getPort())
696 .equals(this.binding.port.getText().toString());
697 }
698
699 protected boolean jidEdited() {
700 final String unmodified;
701 if (mUsernameMode) {
702 unmodified = this.mAccount.getJid().getLocal();
703 } else {
704 unmodified = this.mAccount.getJid().asBareJid().toString();
705 }
706 return !unmodified.equals(this.binding.accountJid.getText().toString());
707 }
708
709 @Override
710 protected String getShareableUri(boolean http) {
711 if (mAccount != null) {
712 return http ? mAccount.getShareableLink() : mAccount.getShareableUri();
713 } else {
714 return null;
715 }
716 }
717
718 @Override
719 protected void onCreate(final Bundle savedInstanceState) {
720 super.onCreate(savedInstanceState);
721 if (savedInstanceState != null) {
722 this.mSavedInstanceAccount = savedInstanceState.getString("account");
723 this.mSavedInstanceInit = savedInstanceState.getBoolean("initMode", false);
724 }
725 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_edit_account);
726 Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
727 setSupportActionBar(binding.toolbar);
728 binding.accountJid.addTextChangedListener(this.mTextWatcher);
729 binding.accountJid.setOnFocusChangeListener(this.mEditTextFocusListener);
730 this.binding.accountPassword.addTextChangedListener(this.mTextWatcher);
731
732 this.binding.avater.setOnClickListener(this.mAvatarClickListener);
733 this.binding.hostname.addTextChangedListener(mTextWatcher);
734 this.binding.hostname.setOnFocusChangeListener(mEditTextFocusListener);
735 this.binding.clearDevices.setOnClickListener(v -> showWipePepDialog());
736 this.binding.port.setText(String.valueOf(Resolver.XMPP_PORT_STARTTLS));
737 this.binding.port.addTextChangedListener(mTextWatcher);
738 this.binding.saveButton.setOnClickListener(this.mSaveButtonClickListener);
739 this.binding.cancelButton.setOnClickListener(this.mCancelButtonClickListener);
740 if (savedInstanceState != null && savedInstanceState.getBoolean("showMoreTable")) {
741 changeMoreTableVisibility(true);
742 }
743 final OnCheckedChangeListener OnCheckedShowConfirmPassword =
744 (buttonView, isChecked) -> updateSaveButton();
745 this.binding.accountRegisterNew.setOnCheckedChangeListener(OnCheckedShowConfirmPassword);
746 if (Config.DISALLOW_REGISTRATION_IN_UI) {
747 this.binding.accountRegisterNew.setVisibility(View.GONE);
748 }
749 this.binding.actionEditYourName.setOnClickListener(this::onEditYourNameClicked);
750 this.binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this));
751 }
752
753 private void onEditYourNameClicked(View view) {
754 quickEdit(
755 mAccount.getDisplayName(),
756 R.string.your_name,
757 value -> {
758 final String displayName = value.trim();
759 updateDisplayName(displayName);
760 mAccount.setDisplayName(displayName);
761 xmppConnectionService.publishDisplayName(mAccount);
762 refreshAvatar();
763 return null;
764 },
765 true);
766 }
767
768 private void refreshAvatar() {
769 AvatarWorkerTask.loadAvatar(
770 mAccount, binding.avater, R.dimen.avatar_on_details_screen_size);
771 }
772
773 @Override
774 public boolean onCreateOptionsMenu(final Menu menu) {
775 super.onCreateOptionsMenu(menu);
776 getMenuInflater().inflate(R.menu.editaccount, menu);
777 final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list);
778 final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
779 final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server);
780 final MenuItem deleteAccount = menu.findItem(R.id.action_delete_account);
781 final MenuItem renewCertificate = menu.findItem(R.id.action_renew_certificate);
782 final MenuItem mamPrefs = menu.findItem(R.id.action_mam_prefs);
783 final MenuItem changePresence = menu.findItem(R.id.action_change_presence);
784 final MenuItem share = menu.findItem(R.id.action_share);
785 renewCertificate.setVisible(mAccount != null && mAccount.getPrivateKeyAlias() != null);
786
787 share.setVisible(mAccount != null && !mInitMode);
788
789 if (mAccount != null && mAccount.isOnlineAndConnected()) {
790 if (!mAccount.getXmppConnection().getFeatures().blocking()) {
791 showBlocklist.setVisible(false);
792 }
793
794 final var registration =
795 mAccount.getXmppConnection().getManager(RegistrationManager.class).hasFeature();
796 changePassword.setVisible(registration);
797 deleteAccount.setVisible(registration);
798 mamPrefs.setVisible(mAccount.getXmppConnection().getFeatures().mam());
799 changePresence.setVisible(!mInitMode);
800 } else {
801 showBlocklist.setVisible(false);
802 showMoreInfo.setVisible(false);
803 changePassword.setVisible(false);
804 deleteAccount.setVisible(false);
805 mamPrefs.setVisible(false);
806 changePresence.setVisible(false);
807 }
808 return super.onCreateOptionsMenu(menu);
809 }
810
811 @Override
812 public boolean onPrepareOptionsMenu(Menu menu) {
813 final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
814 if (showMoreInfo.isVisible()) {
815 showMoreInfo.setChecked(binding.serverInfoMore.getVisibility() == View.VISIBLE);
816 }
817 return super.onPrepareOptionsMenu(menu);
818 }
819
820 @Override
821 public void onStart() {
822 super.onStart();
823 final Intent intent = getIntent();
824 if (intent != null) {
825 try {
826 this.jidToEdit = Jid.of(intent.getStringExtra("jid"));
827 } catch (final IllegalArgumentException | NullPointerException ignored) {
828 this.jidToEdit = null;
829 }
830 final Uri data = intent.getData();
831 final XmppUri xmppUri = data == null ? null : new XmppUri(data);
832 final boolean scanned = intent.getBooleanExtra("scanned", false);
833 if (jidToEdit != null && xmppUri != null && xmppUri.hasFingerprints()) {
834 if (scanned) {
835 if (xmppConnectionServiceBound) {
836 processFingerprintVerification(xmppUri, false);
837 } else {
838 this.pendingUri = xmppUri;
839 }
840 } else {
841 displayVerificationWarningDialog(xmppUri);
842 }
843 }
844 boolean init = intent.getBooleanExtra("init", false);
845 boolean openedFromNotification =
846 intent.getBooleanExtra(EXTRA_OPENED_FROM_NOTIFICATION, false);
847 Log.d(Config.LOGTAG, "extras " + intent.getExtras());
848 this.mForceRegister =
849 intent.hasExtra(EXTRA_FORCE_REGISTER)
850 ? intent.getBooleanExtra(EXTRA_FORCE_REGISTER, false)
851 : null;
852 Log.d(Config.LOGTAG, "force register=" + mForceRegister);
853 this.mInitMode = init || this.jidToEdit == null;
854 this.messageFingerprint = intent.getStringExtra("fingerprint");
855 if (!mInitMode) {
856 this.binding.accountRegisterNew.setVisibility(View.GONE);
857 setTitle(getString(R.string.account_details));
858 configureActionBar(getSupportActionBar(), !openedFromNotification);
859 } else {
860 this.binding.avater.setVisibility(View.GONE);
861 configureActionBar(
862 getSupportActionBar(), !(init && Config.MAGIC_CREATE_DOMAIN == null));
863 if (mForceRegister != null) {
864 if (mForceRegister) {
865 setTitle(R.string.register_new_account);
866 } else {
867 setTitle(R.string.add_existing_account);
868 }
869 } else {
870 setTitle(R.string.action_add_account);
871 }
872 }
873 }
874 SharedPreferences preferences = getPreferences();
875 mUseTor =
876 QuickConversationsService.isConversations()
877 && preferences.getBoolean(
878 "use_tor", getResources().getBoolean(R.bool.use_tor));
879 this.mShowOptions =
880 mUseTor
881 || (QuickConversationsService.isConversations()
882 && preferences.getBoolean(
883 "show_connection_options",
884 getResources().getBoolean(R.bool.show_connection_options)));
885 this.binding.namePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE);
886 if (mForceRegister != null) {
887 this.binding.accountRegisterNew.setVisibility(View.GONE);
888 }
889 }
890
891 private void displayVerificationWarningDialog(final XmppUri xmppUri) {
892 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
893 builder.setTitle(R.string.verify_omemo_keys);
894 View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
895 final CheckBox isTrustedSource = view.findViewById(R.id.trusted_source);
896 TextView warning = view.findViewById(R.id.warning);
897 warning.setText(R.string.verifying_omemo_keys_trusted_source_account);
898 builder.setView(view);
899 builder.setPositiveButton(
900 R.string.continue_btn,
901 (dialog, which) -> {
902 if (isTrustedSource.isChecked()) {
903 processFingerprintVerification(xmppUri, false);
904 } else {
905 finish();
906 }
907 });
908 builder.setNegativeButton(R.string.cancel, (dialog, which) -> finish());
909 final var dialog = builder.create();
910 dialog.setCanceledOnTouchOutside(false);
911 dialog.setOnCancelListener(d -> finish());
912 dialog.show();
913 }
914
915 @Override
916 public void onNewIntent(@NonNull final Intent intent) {
917 super.onNewIntent(intent);
918 if (intent.getData() != null) {
919 final XmppUri uri = new XmppUri(intent.getData());
920 if (xmppConnectionServiceBound) {
921 processFingerprintVerification(uri, false);
922 } else {
923 this.pendingUri = uri;
924 }
925 }
926 }
927
928 @Override
929 public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) {
930 if (mAccount != null) {
931 savedInstanceState.putString("account", mAccount.getJid().asBareJid().toString());
932 savedInstanceState.putBoolean("initMode", mInitMode);
933 savedInstanceState.putBoolean(
934 "showMoreTable", binding.serverInfoMore.getVisibility() == View.VISIBLE);
935 }
936 super.onSaveInstanceState(savedInstanceState);
937 }
938
939 protected void onBackendConnected() {
940 boolean init = true;
941 if (mSavedInstanceAccount != null) {
942 try {
943 this.mAccount =
944 xmppConnectionService.findAccountByJid(Jid.of(mSavedInstanceAccount));
945 this.mInitMode = mSavedInstanceInit;
946 init = false;
947 } catch (IllegalArgumentException e) {
948 this.mAccount = null;
949 }
950
951 } else if (this.jidToEdit != null) {
952 this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit);
953 }
954
955 if (mAccount != null) {
956 this.mInitMode |= this.mAccount.isOptionSet(Account.OPTION_REGISTER);
957 this.mUsernameMode |=
958 mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)
959 && mAccount.isOptionSet(Account.OPTION_REGISTER);
960 if (mPendingFingerprintVerificationUri != null) {
961 processFingerprintVerification(mPendingFingerprintVerificationUri, false);
962 mPendingFingerprintVerificationUri = null;
963 }
964 updateAccountInformation(init);
965 }
966
967 if (Config.MAGIC_CREATE_DOMAIN == null
968 && this.xmppConnectionService.getAccounts().size() == 0) {
969 this.binding.cancelButton.setEnabled(false);
970 }
971 if (mUsernameMode) {
972 this.binding.accountJidLayout.setHint(getString(R.string.username_hint));
973 } else {
974 final KnownHostsAdapter mKnownHostsAdapter =
975 new KnownHostsAdapter(
976 this,
977 R.layout.item_autocomplete,
978 xmppConnectionService.getKnownHosts());
979 this.binding.accountJid.setAdapter(mKnownHostsAdapter);
980 }
981
982 if (pendingUri != null) {
983 processFingerprintVerification(pendingUri, false);
984 pendingUri = null;
985 }
986 updatePortLayout();
987 updateSaveButton();
988 invalidateOptionsMenu();
989 }
990
991 private String getUserModeDomain() {
992 if (mAccount != null && mAccount.getJid().getDomain() != null) {
993 return mAccount.getServer();
994 } else {
995 return null;
996 }
997 }
998
999 @Override
1000 public boolean onOptionsItemSelected(final MenuItem item) {
1001 if (MenuDoubleTabUtil.shouldIgnoreTap()) {
1002 return false;
1003 }
1004 switch (item.getItemId()) {
1005 case android.R.id.home:
1006 deleteAccountAndReturnIfNecessary();
1007 break;
1008 case R.id.action_show_block_list:
1009 final Intent showBlocklistIntent = new Intent(this, BlocklistActivity.class);
1010 showBlocklistIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toString());
1011 startActivity(showBlocklistIntent);
1012 break;
1013 case R.id.action_server_info_show_more:
1014 changeMoreTableVisibility(!item.isChecked());
1015 break;
1016 case R.id.action_share_barcode:
1017 shareBarcode();
1018 break;
1019 case R.id.action_share_http:
1020 shareLink(true);
1021 break;
1022 case R.id.action_share_uri:
1023 shareLink(false);
1024 break;
1025 case R.id.action_change_password_on_server:
1026 gotoChangePassword();
1027 break;
1028 case R.id.action_delete_account:
1029 deleteAccount();
1030 break;
1031 case R.id.action_mam_prefs:
1032 editMamPrefs();
1033 break;
1034 case R.id.action_renew_certificate:
1035 renewCertificate();
1036 break;
1037 case R.id.action_change_presence:
1038 changePresence();
1039 break;
1040 }
1041 return super.onOptionsItemSelected(item);
1042 }
1043
1044 private void deleteAccount() {
1045 this.deleteAccount(mAccount, () -> finish());
1046 }
1047
1048 private boolean inNeedOfSaslAccept() {
1049 return mAccount != null
1050 && mAccount.getLastErrorStatus() == Account.State.DOWNGRADE_ATTACK
1051 && mAccount.getPinnedMechanismPriority() >= 0
1052 && !accountInfoEdited();
1053 }
1054
1055 private void shareBarcode() {
1056 Intent intent = new Intent(Intent.ACTION_SEND);
1057 intent.putExtra(Intent.EXTRA_STREAM, BarcodeProvider.getUriForAccount(this, mAccount));
1058 intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1059 intent.setType("image/png");
1060 startActivity(Intent.createChooser(intent, getText(R.string.share_with)));
1061 }
1062
1063 private void changeMoreTableVisibility(final boolean visible) {
1064 binding.serverInfoMore.setVisibility(visible ? View.VISIBLE : View.GONE);
1065 binding.serverInfoLoginMechanism.setVisibility(visible ? View.VISIBLE : View.GONE);
1066 }
1067
1068 private void gotoChangePassword() {
1069 final Intent changePasswordIntent = new Intent(this, ChangePasswordActivity.class);
1070 changePasswordIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toString());
1071 startActivity(changePasswordIntent);
1072 }
1073
1074 private void renewCertificate() {
1075 KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
1076 }
1077
1078 private void changePresence() {
1079 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1080 boolean manualStatus =
1081 sharedPreferences.getBoolean(
1082 AppSettings.MANUALLY_CHANGE_PRESENCE,
1083 getResources().getBoolean(R.bool.manually_change_presence));
1084 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1085 final DialogPresenceBinding binding =
1086 DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_presence, null, false);
1087 String current = mAccount.getPresenceStatusMessage();
1088 if (current != null && !current.trim().isEmpty()) {
1089 binding.statusMessage.append(current);
1090 }
1091 setAvailabilityRadioButton(mAccount.getPresenceStatus(), binding);
1092 binding.show.setVisibility(manualStatus ? View.VISIBLE : View.GONE);
1093 List<PresenceTemplate> templates = xmppConnectionService.getPresenceTemplates(mAccount);
1094 PresenceTemplateAdapter presenceTemplateAdapter =
1095 new PresenceTemplateAdapter(this, R.layout.item_autocomplete, templates);
1096 binding.statusMessage.setAdapter(presenceTemplateAdapter);
1097 binding.statusMessage.setOnItemClickListener(
1098 (parent, view, position, id) -> {
1099 PresenceTemplate template =
1100 (PresenceTemplate) parent.getItemAtPosition(position);
1101 setAvailabilityRadioButton(template.getStatus(), binding);
1102 });
1103 builder.setTitle(R.string.edit_status_message_title);
1104 builder.setView(binding.getRoot());
1105 builder.setNegativeButton(R.string.cancel, null);
1106 builder.setPositiveButton(
1107 R.string.confirm,
1108 (dialog, which) -> {
1109 PresenceTemplate template =
1110 new PresenceTemplate(
1111 getAvailabilityRadioButton(binding),
1112 binding.statusMessage.getText().toString().trim());
1113 if (mAccount.getPgpId() != 0 && hasPgp()) {
1114 generateSignature(null, template);
1115 } else {
1116 xmppConnectionService.changeStatus(mAccount, template, null);
1117 }
1118 });
1119 builder.create().show();
1120 }
1121
1122 private void generateSignature(Intent intent, PresenceTemplate template) {
1123 xmppConnectionService
1124 .getPgpEngine()
1125 .generateSignature(
1126 intent,
1127 mAccount,
1128 template.getStatusMessage(),
1129 new UiCallback<String>() {
1130 @Override
1131 public void success(String signature) {
1132 xmppConnectionService.changeStatus(mAccount, template, signature);
1133 }
1134
1135 @Override
1136 public void error(int errorCode, String object) {}
1137
1138 @Override
1139 public void userInputRequired(PendingIntent pi, String object) {
1140 mPendingPresenceTemplate.push(template);
1141 try {
1142 startIntentSenderForResult(
1143 pi.getIntentSender(),
1144 REQUEST_CHANGE_STATUS,
1145 null,
1146 0,
1147 0,
1148 0,
1149 Compatibility.pgpStartIntentSenderOptions());
1150 } catch (final IntentSender.SendIntentException ignored) {
1151 }
1152 }
1153 });
1154 }
1155
1156 @Override
1157 public void alias(String alias) {
1158 if (alias != null) {
1159 xmppConnectionService.updateKeyInAccount(mAccount, alias);
1160 }
1161 }
1162
1163 private void updateAccountInformation(boolean init) {
1164 if (init) {
1165 this.binding.accountJid.getEditableText().clear();
1166 if (mUsernameMode) {
1167 this.binding.accountJid.getEditableText().append(this.mAccount.getJid().getLocal());
1168 } else {
1169 this.binding
1170 .accountJid
1171 .getEditableText()
1172 .append(this.mAccount.getJid().asBareJid().toString());
1173 }
1174 this.binding.accountPassword.getEditableText().clear();
1175 this.binding.accountPassword.getEditableText().append(this.mAccount.getPassword());
1176 this.binding.hostname.setText("");
1177 this.binding.hostname.getEditableText().append(this.mAccount.getHostname());
1178 this.binding.port.setText("");
1179 this.binding.port.getEditableText().append(String.valueOf(this.mAccount.getPort()));
1180 this.binding.namePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE);
1181 }
1182
1183 final boolean editable =
1184 !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)
1185 && !mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME)
1186 && QuickConversationsService.isConversations();
1187 this.binding.accountJid.setEnabled(editable);
1188 this.binding.accountJid.setFocusable(editable);
1189 this.binding.accountJid.setFocusableInTouchMode(editable);
1190 this.binding.accountJid.setCursorVisible(editable);
1191
1192 final String displayName = mAccount.getDisplayName();
1193 updateDisplayName(displayName);
1194
1195 final boolean togglePassword =
1196 mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)
1197 || !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
1198 final boolean neverLoggedIn =
1199 !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)
1200 && QuickConversationsService.isConversations();
1201 final boolean editPassword = mAccount.unauthorized() || neverLoggedIn;
1202
1203 this.binding.accountPasswordLayout.setPasswordVisibilityToggleEnabled(togglePassword);
1204
1205 this.binding.accountPassword.setFocusable(editPassword);
1206 this.binding.accountPassword.setFocusableInTouchMode(editPassword);
1207 this.binding.accountPassword.setCursorVisible(editPassword);
1208 this.binding.accountPassword.setEnabled(editPassword);
1209
1210 if (!mInitMode) {
1211 this.binding.avater.setVisibility(View.VISIBLE);
1212 AvatarWorkerTask.loadAvatar(
1213 mAccount, binding.avater, R.dimen.avatar_on_details_screen_size);
1214 } else {
1215 this.binding.avater.setVisibility(View.GONE);
1216 }
1217 this.binding.accountRegisterNew.setChecked(
1218 this.mAccount.isOptionSet(Account.OPTION_REGISTER));
1219 if (this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) {
1220 if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) {
1221 ActionBar actionBar = getSupportActionBar();
1222 if (actionBar != null) {
1223 actionBar.setTitle(R.string.create_account);
1224 }
1225 }
1226 this.binding.accountRegisterNew.setVisibility(View.GONE);
1227 } else if (this.mAccount.isOptionSet(Account.OPTION_REGISTER) && mForceRegister == null) {
1228 this.binding.accountRegisterNew.setVisibility(View.VISIBLE);
1229 } else {
1230 this.binding.accountRegisterNew.setVisibility(View.GONE);
1231 }
1232 if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) {
1233 final var connection = this.mAccount.getXmppConnection();
1234 final Features features = connection.getFeatures();
1235 this.binding.stats.setVisibility(View.VISIBLE);
1236 boolean showBatteryWarning = isOptimizingBattery();
1237 boolean showDataSaverWarning = isAffectedByDataSaver();
1238 showOsOptimizationWarning(showBatteryWarning, showDataSaverWarning);
1239 this.binding.sessionEst.setText(
1240 UIHelper.readableTimeDifferenceFull(
1241 this, this.mAccount.getXmppConnection().getLastSessionEstablished()));
1242 if (features.rosterVersioning()) {
1243 this.binding.serverInfoRosterVersion.setText(R.string.server_info_available);
1244 } else {
1245 this.binding.serverInfoRosterVersion.setText(R.string.server_info_unavailable);
1246 }
1247 if (connection.getManager(CarbonsManager.class).isEnabled()) {
1248 this.binding.serverInfoCarbons.setText(R.string.server_info_available);
1249 } else {
1250 this.binding.serverInfoCarbons.setText(R.string.server_info_unavailable);
1251 }
1252 if (features.mam()) {
1253 this.binding.serverInfoMam.setText(R.string.server_info_available);
1254 } else {
1255 this.binding.serverInfoMam.setText(R.string.server_info_unavailable);
1256 }
1257 if (features.csi()) {
1258 this.binding.serverInfoCsi.setText(R.string.server_info_available);
1259 } else {
1260 this.binding.serverInfoCsi.setText(R.string.server_info_unavailable);
1261 }
1262 if (features.blocking()) {
1263 this.binding.serverInfoBlocking.setText(R.string.server_info_available);
1264 } else {
1265 this.binding.serverInfoBlocking.setText(R.string.server_info_unavailable);
1266 }
1267 if (features.sm()) {
1268 this.binding.serverInfoSm.setText(R.string.server_info_available);
1269 } else {
1270 this.binding.serverInfoSm.setText(R.string.server_info_unavailable);
1271 }
1272 if (features.externalServiceDiscovery()) {
1273 this.binding.serverInfoExternalService.setText(R.string.server_info_available);
1274 } else {
1275 this.binding.serverInfoExternalService.setText(R.string.server_info_unavailable);
1276 }
1277 if (features.bind2()) {
1278 this.binding.serverInfoBind2.setText(R.string.server_info_available);
1279 } else {
1280 this.binding.serverInfoBind2.setText(R.string.server_info_unavailable);
1281 }
1282 if (features.sasl2()) {
1283 this.binding.serverInfoSasl2.setText(R.string.server_info_available);
1284 } else {
1285 this.binding.serverInfoSasl2.setText(R.string.server_info_unavailable);
1286 }
1287 this.binding.loginMechanism.setText(Strings.nullToEmpty(features.loginMechanism()));
1288 if (features.pep()) {
1289 AxolotlService axolotlService = this.mAccount.getAxolotlService();
1290 if (axolotlService != null && axolotlService.isPepBroken()) {
1291 this.binding.serverInfoPep.setText(R.string.server_info_broken);
1292 } else if (features.pepPublishOptions() || features.pepOmemoWhitelisted()) {
1293 this.binding.serverInfoPep.setText(R.string.server_info_available);
1294 } else {
1295 this.binding.serverInfoPep.setText(R.string.server_info_partial);
1296 }
1297 } else {
1298 this.binding.serverInfoPep.setText(R.string.server_info_unavailable);
1299 }
1300 final var httpUploadManager = connection.getManager(HttpUploadManager.class);
1301 final var uploadService = httpUploadManager.getService();
1302 if (uploadService != null) {
1303 final Long maxFileSize = uploadService.getMaxFileSize();
1304 if (maxFileSize == null) {
1305 this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
1306 } else {
1307 this.binding.serverInfoHttpUpload.setText(
1308 UIHelper.filesizeToString(maxFileSize));
1309 }
1310 } else {
1311 this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable);
1312 }
1313
1314 this.binding.pushRow.setVisibility(
1315 xmppConnectionService.getPushManagementService().isStub()
1316 ? View.GONE
1317 : View.VISIBLE);
1318
1319 if (xmppConnectionService.getPushManagementService().available(mAccount)) {
1320 this.binding.serverInfoPush.setText(R.string.server_info_available);
1321 } else {
1322 this.binding.serverInfoPush.setText(R.string.server_info_unavailable);
1323 }
1324 final long pgpKeyId = this.mAccount.getPgpId();
1325 if (pgpKeyId != 0 && Config.supportOpenPgp()) {
1326 OnClickListener openPgp = view -> launchOpenKeyChain(pgpKeyId);
1327 OnClickListener delete = view -> showDeletePgpDialog();
1328 this.binding.pgpFingerprintBox.setVisibility(View.VISIBLE);
1329 this.binding.pgpFingerprint.setText(OpenPgpUtils.convertKeyIdToHex(pgpKeyId));
1330 this.binding.pgpFingerprint.setOnClickListener(openPgp);
1331 if ("pgp".equals(messageFingerprint)) {
1332 this.binding.pgpFingerprintDesc.setTextColor(
1333 MaterialColors.getColor(
1334 binding.pgpFingerprintDesc,
1335 com.google.android.material.R.attr.colorPrimaryVariant));
1336 }
1337 this.binding.pgpFingerprintDesc.setOnClickListener(openPgp);
1338 this.binding.actionDeletePgp.setOnClickListener(delete);
1339 } else {
1340 this.binding.pgpFingerprintBox.setVisibility(View.GONE);
1341 }
1342 final String ownAxolotlFingerprint =
1343 this.mAccount.getAxolotlService().getOwnFingerprint();
1344 if (ownAxolotlFingerprint != null && Config.supportOmemo()) {
1345 this.binding.axolotlFingerprintBox.setVisibility(View.VISIBLE);
1346 this.binding.axolotlFingerprintBox.setOnCreateContextMenuListener(
1347 (menu, v, menuInfo) -> {
1348 getMenuInflater().inflate(R.menu.omemo_key_context, menu);
1349 menu.findItem(R.id.verify_scan).setVisible(false);
1350 menu.findItem(R.id.distrust_key).setVisible(false);
1351 this.mSelectedFingerprint = ownAxolotlFingerprint;
1352 });
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 this.binding.serviceOutage.setVisibility(View.GONE);
1407 } else {
1408 final TextInputLayout errorLayout;
1409 final var status = this.mAccount.getStatus();
1410 if (status.isError()
1411 || Arrays.asList(
1412 Account.State.NO_INTERNET,
1413 Account.State.MISSING_INTERNET_PERMISSION)
1414 .contains(status)) {
1415 if (status == Account.State.UNAUTHORIZED
1416 || status == Account.State.DOWNGRADE_ATTACK) {
1417 errorLayout = this.binding.accountPasswordLayout;
1418 } else if (mShowOptions
1419 && status == Account.State.SERVER_NOT_FOUND
1420 && this.binding.hostname.getText().length() > 0) {
1421 errorLayout = this.binding.hostnameLayout;
1422 } else {
1423 errorLayout = this.binding.accountJidLayout;
1424 }
1425 errorLayout.setError(getString(this.mAccount.getStatus().getReadableId()));
1426 if (init || !accountInfoEdited()) {
1427 errorLayout.requestFocus();
1428 }
1429 } else {
1430 errorLayout = null;
1431 }
1432 removeErrorsOnAllBut(errorLayout);
1433 this.binding.stats.setVisibility(View.GONE);
1434 this.binding.otherDeviceKeysCard.setVisibility(View.GONE);
1435 final var sos = mAccount.getServiceOutageStatus();
1436 if (mAccount.isServiceOutage() && sos != null) {
1437 this.binding.serviceOutage.setVisibility(View.VISIBLE);
1438 if (sos.isPlanned()) {
1439 this.binding.sosTitle.setText(R.string.account_status_service_outage_scheduled);
1440 } else {
1441 this.binding.sosTitle.setText(R.string.account_status_service_outage_known);
1442 }
1443 final var sosMessage = sos.getMessage();
1444 if (Strings.isNullOrEmpty(sosMessage)) {
1445 this.binding.sosMessage.setVisibility(View.GONE);
1446 } else {
1447 final var sosMessageSpannable = new SpannableString(sosMessage);
1448 Linkify.addLinks(sosMessageSpannable);
1449 FixedURLSpan.fix(sosMessageSpannable);
1450 this.binding.sosMessage.setText(sosMessageSpannable);
1451 this.binding.sosMessage.setVisibility(View.VISIBLE);
1452 this.binding.sosMessage.setMovementMethod(LinkMovementMethod.getInstance());
1453 }
1454 final var expectedEnd = sos.getExpectedEnd();
1455 if (expectedEnd <= 0) {
1456 this.binding.sosScheduledEnd.setVisibility(View.GONE);
1457 } else {
1458 this.binding.sosScheduledEnd.setVisibility(View.VISIBLE);
1459 this.binding.sosScheduledEnd.setText(
1460 getString(
1461 R.string.sos_scheduled_return,
1462 DateUtils.formatDateTime(
1463 this,
1464 expectedEnd,
1465 DateUtils.FORMAT_SHOW_TIME
1466 | DateUtils.FORMAT_NUMERIC_DATE
1467 | DateUtils.FORMAT_SHOW_YEAR
1468 | DateUtils.FORMAT_SHOW_DATE)));
1469 }
1470 } else {
1471 this.binding.serviceOutage.setVisibility(View.GONE);
1472 }
1473 }
1474 }
1475
1476 private void updateDisplayName(String displayName) {
1477 if (TextUtils.isEmpty(displayName)) {
1478 this.binding.yourName.setText(R.string.no_name_set_instructions);
1479 this.binding.yourName.setTextColor(
1480 MaterialColors.getColor(
1481 binding.yourName,
1482 com.google.android.material.R.attr.colorOnSurfaceVariant));
1483 } else {
1484 this.binding.yourName.setText(displayName);
1485 this.binding.yourName.setTextColor(
1486 MaterialColors.getColor(
1487 binding.yourName,
1488 com.google.android.material.R.attr.colorOnSurfaceVariant));
1489 }
1490 }
1491
1492 private void removeErrorsOnAllBut(TextInputLayout exception) {
1493 if (this.binding.accountJidLayout != exception) {
1494 this.binding.accountJidLayout.setErrorEnabled(false);
1495 this.binding.accountJidLayout.setError(null);
1496 }
1497 if (this.binding.accountPasswordLayout != exception) {
1498 this.binding.accountPasswordLayout.setErrorEnabled(false);
1499 this.binding.accountPasswordLayout.setError(null);
1500 }
1501 if (this.binding.hostnameLayout != exception) {
1502 this.binding.hostnameLayout.setErrorEnabled(false);
1503 this.binding.hostnameLayout.setError(null);
1504 }
1505 if (this.binding.portLayout != exception) {
1506 this.binding.portLayout.setErrorEnabled(false);
1507 this.binding.portLayout.setError(null);
1508 }
1509 }
1510
1511 private void showDeletePgpDialog() {
1512 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1513 builder.setTitle(R.string.unpublish_pgp);
1514 builder.setMessage(R.string.unpublish_pgp_message);
1515 builder.setNegativeButton(R.string.cancel, null);
1516 builder.setPositiveButton(
1517 R.string.confirm,
1518 (dialogInterface, i) -> {
1519 mAccount.setPgpSignId(0);
1520 mAccount.unsetPgpSignature();
1521 xmppConnectionService.databaseBackend.updateAccount(mAccount);
1522 mAccount.getXmppConnection().getManager(PresenceManager.class).available();
1523 refreshUiReal();
1524 });
1525 builder.create().show();
1526 }
1527
1528 private void showOsOptimizationWarning(
1529 boolean showBatteryWarning, boolean showDataSaverWarning) {
1530 this.binding.osOptimization.setVisibility(
1531 showBatteryWarning || showDataSaverWarning ? View.VISIBLE : View.GONE);
1532 if (showDataSaverWarning
1533 && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
1534 this.binding.osOptimizationHeadline.setText(R.string.data_saver_enabled);
1535 this.binding.osOptimizationBody.setText(
1536 getString(R.string.data_saver_enabled_explained, getString(R.string.app_name)));
1537 this.binding.osOptimizationDisable.setText(R.string.allow);
1538 this.binding.osOptimizationDisable.setOnClickListener(
1539 v -> {
1540 Intent intent =
1541 new Intent(
1542 Settings
1543 .ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS);
1544 Uri uri = Uri.parse("package:" + getPackageName());
1545 intent.setData(uri);
1546 try {
1547 startActivityForResult(intent, REQUEST_DATA_SAVER);
1548 } catch (ActivityNotFoundException e) {
1549 Toast.makeText(
1550 EditAccountActivity.this,
1551 getString(
1552 R.string.device_does_not_support_data_saver,
1553 getString(R.string.app_name)),
1554 Toast.LENGTH_SHORT)
1555 .show();
1556 }
1557 });
1558 } else if (showBatteryWarning) {
1559 this.binding.osOptimizationDisable.setText(R.string.disable);
1560 this.binding.osOptimizationHeadline.setText(R.string.battery_optimizations_enabled);
1561 this.binding.osOptimizationBody.setText(
1562 getString(
1563 R.string.battery_optimizations_enabled_explained,
1564 getString(R.string.app_name)));
1565 this.binding.osOptimizationDisable.setOnClickListener(
1566 v -> {
1567 Intent intent =
1568 new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
1569 Uri uri = Uri.parse("package:" + getPackageName());
1570 intent.setData(uri);
1571 try {
1572 startActivityForResult(intent, REQUEST_BATTERY_OP);
1573 } catch (ActivityNotFoundException e) {
1574 Toast.makeText(
1575 EditAccountActivity.this,
1576 R.string.device_does_not_support_battery_op,
1577 Toast.LENGTH_SHORT)
1578 .show();
1579 }
1580 });
1581 }
1582 }
1583
1584 public void showWipePepDialog() {
1585 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1586 builder.setTitle(getString(R.string.clear_other_devices));
1587 builder.setIconAttribute(android.R.attr.alertDialogIcon);
1588 builder.setMessage(getString(R.string.clear_other_devices_desc));
1589 builder.setNegativeButton(getString(R.string.cancel), null);
1590 builder.setPositiveButton(
1591 getString(R.string.accept),
1592 (dialog, which) -> mAccount.getAxolotlService().wipeOtherPepDevices());
1593 builder.create().show();
1594 }
1595
1596 private void editMamPrefs() {
1597 this.mFetchingMamPrefsToast =
1598 Toast.makeText(this, R.string.fetching_mam_prefs, Toast.LENGTH_LONG);
1599 this.mFetchingMamPrefsToast.show();
1600 xmppConnectionService.fetchMamPreferences(mAccount, this);
1601 }
1602
1603 @Override
1604 public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
1605 refreshUi();
1606 }
1607
1608 @Override
1609 public void onCaptchaRequested(final Account account, final Data data, final Bitmap captcha) {
1610 runOnUiThread(
1611 () -> {
1612 if (mCaptchaDialog != null && mCaptchaDialog.isShowing()) {
1613 mCaptchaDialog.dismiss();
1614 }
1615 if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
1616 Log.d(Config.LOGTAG, "activity not running when captcha was requested");
1617 return;
1618 }
1619 final MaterialAlertDialogBuilder builder =
1620 new MaterialAlertDialogBuilder(EditAccountActivity.this);
1621 final View view = getLayoutInflater().inflate(R.layout.captcha, null);
1622 final ImageView imageView = view.findViewById(R.id.captcha);
1623 final EditText input = view.findViewById(R.id.input);
1624 imageView.setImageBitmap(captcha);
1625
1626 builder.setTitle(getString(R.string.captcha_required));
1627 builder.setView(view);
1628
1629 builder.setPositiveButton(
1630 getString(R.string.ok),
1631 (dialog, which) ->
1632 account.getXmppConnection()
1633 .register(data, input.getText().toString()));
1634 builder.setNegativeButton(
1635 getString(R.string.cancel),
1636 (dialog, which) -> account.getXmppConnection().cancelRegistration());
1637
1638 builder.setOnCancelListener(
1639 dialog -> account.getXmppConnection().cancelRegistration());
1640 mCaptchaDialog = builder.create();
1641 mCaptchaDialog.show();
1642 input.requestFocus();
1643 });
1644 }
1645
1646 public void onShowErrorToast(final int resId) {
1647 runOnUiThread(
1648 () -> Toast.makeText(EditAccountActivity.this, resId, Toast.LENGTH_SHORT).show());
1649 }
1650
1651 @Override
1652 public void onPreferencesFetched(final Element prefs) {
1653 runOnUiThread(
1654 () -> {
1655 if (mFetchingMamPrefsToast != null) {
1656 mFetchingMamPrefsToast.cancel();
1657 }
1658 final MaterialAlertDialogBuilder builder =
1659 new MaterialAlertDialogBuilder(EditAccountActivity.this);
1660 builder.setTitle(R.string.server_side_mam_prefs);
1661 String defaultAttr = prefs.getAttribute("default");
1662 final List<String> defaults = Arrays.asList("never", "roster", "always");
1663 final AtomicInteger choice =
1664 new AtomicInteger(Math.max(0, defaults.indexOf(defaultAttr)));
1665 builder.setSingleChoiceItems(
1666 R.array.mam_prefs, choice.get(), (dialog, which) -> choice.set(which));
1667 builder.setNegativeButton(R.string.cancel, null);
1668 builder.setPositiveButton(
1669 R.string.ok,
1670 (dialog, which) -> {
1671 prefs.setAttribute("default", defaults.get(choice.get()));
1672 xmppConnectionService.pushMamPreferences(mAccount, prefs);
1673 });
1674 if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
1675 builder.create().show();
1676 }
1677 });
1678 }
1679
1680 @Override
1681 public void onPreferencesFetchFailed() {
1682 runOnUiThread(
1683 () -> {
1684 if (mFetchingMamPrefsToast != null) {
1685 mFetchingMamPrefsToast.cancel();
1686 }
1687 Toast.makeText(
1688 EditAccountActivity.this,
1689 R.string.unable_to_fetch_mam_prefs,
1690 Toast.LENGTH_LONG)
1691 .show();
1692 });
1693 }
1694
1695 @Override
1696 public void OnUpdateBlocklist(Status status) {
1697 refreshUi();
1698 }
1699}