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