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