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