2014-08-15 15:31:24 +00:00
package eu.siacs.conversations.ui ;
2018-03-04 10:43:58 +00:00
import android.app.Activity ;
2014-08-15 15:31:24 +00:00
import android.app.PendingIntent ;
2016-04-29 11:58:37 +00:00
import android.content.ActivityNotFoundException ;
2014-08-15 15:31:24 +00:00
import android.content.Intent ;
2018-03-04 10:43:58 +00:00
import android.content.IntentSender ;
2016-01-25 20:17:53 +00:00
import android.content.SharedPreferences ;
2018-02-18 08:06:37 +00:00
import android.databinding.DataBindingUtil ;
2015-10-11 11:11:50 +00:00
import android.graphics.Bitmap ;
2016-01-11 10:17:45 +00:00
import android.net.Uri ;
2020-11-16 11:08:07 +00:00
import android.os.Build ;
2014-08-15 15:31:24 +00:00
import android.os.Bundle ;
2018-02-17 06:09:21 +00:00
import android.os.Handler ;
2018-03-03 11:27:46 +00:00
import android.preference.PreferenceManager ;
2016-01-11 10:17:45 +00:00
import android.provider.Settings ;
2015-10-11 13:48:58 +00:00
import android.security.KeyChain ;
import android.security.KeyChainAliasCallback ;
2018-02-18 03:20:02 +00:00
import android.support.design.widget.TextInputLayout ;
2018-02-16 11:30:46 +00:00
import android.support.v7.app.ActionBar ;
2018-02-17 06:09:21 +00:00
import android.support.v7.app.AlertDialog ;
import android.support.v7.app.AlertDialog.Builder ;
2018-03-18 08:30:22 +00:00
import android.support.v7.widget.Toolbar ;
2014-10-05 10:05:27 +00:00
import android.text.Editable ;
2018-08-29 07:33:02 +00:00
import android.text.TextUtils ;
2014-10-05 10:05:27 +00:00
import android.text.TextWatcher ;
2018-03-03 11:27:46 +00:00
import android.util.Log ;
2014-11-03 21:47:07 +00:00
import android.view.Menu ;
import android.view.MenuItem ;
2014-08-15 15:31:24 +00:00
import android.view.View ;
import android.view.View.OnClickListener ;
2014-11-03 21:47:07 +00:00
import android.widget.CompoundButton.OnCheckedChangeListener ;
2014-08-15 15:31:24 +00:00
import android.widget.EditText ;
2014-11-03 21:47:07 +00:00
import android.widget.ImageView ;
2014-10-03 13:55:06 +00:00
import android.widget.Toast ;
2014-11-03 21:47:07 +00:00
2020-08-01 13:03:20 +00:00
import com.google.common.base.CharMatcher ;
2017-05-04 11:02:46 +00:00
import org.openintents.openpgp.util.OpenPgpUtils ;
2017-12-08 13:23:38 +00:00
import java.net.URL ;
2016-02-09 12:01:17 +00:00
import java.util.Arrays ;
import java.util.List ;
2015-07-07 17:36:22 +00:00
import java.util.Set ;
2018-12-03 12:15:06 +00:00
import java.util.concurrent.atomic.AtomicBoolean ;
2016-02-09 12:01:17 +00:00
import java.util.concurrent.atomic.AtomicInteger ;
2015-07-07 17:36:22 +00:00
2015-07-21 23:00:20 +00:00
import eu.siacs.conversations.Config ;
2014-08-15 15:31:24 +00:00
import eu.siacs.conversations.R ;
2015-09-06 17:40:28 +00:00
import eu.siacs.conversations.crypto.axolotl.AxolotlService ;
2016-11-18 20:49:52 +00:00
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession ;
2018-02-18 08:06:37 +00:00
import eu.siacs.conversations.databinding.ActivityEditAccountBinding ;
2018-03-03 11:27:46 +00:00
import eu.siacs.conversations.databinding.DialogPresenceBinding ;
2014-08-15 15:31:24 +00:00
import eu.siacs.conversations.entities.Account ;
2018-03-04 10:43:58 +00:00
import eu.siacs.conversations.entities.Presence ;
2018-03-03 11:27:46 +00:00
import eu.siacs.conversations.entities.PresenceTemplate ;
2016-11-22 21:31:46 +00:00
import eu.siacs.conversations.services.BarcodeProvider ;
2018-10-24 11:54:57 +00:00
import eu.siacs.conversations.services.QuickConversationsService ;
2015-10-11 13:48:58 +00:00
import eu.siacs.conversations.services.XmppConnectionService ;
2014-08-15 15:31:24 +00:00
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate ;
2018-02-17 06:09:21 +00:00
import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested ;
2014-08-15 15:31:24 +00:00
import eu.siacs.conversations.ui.adapter.KnownHostsAdapter ;
2018-03-03 11:27:46 +00:00
import eu.siacs.conversations.ui.adapter.PresenceTemplateAdapter ;
2019-01-25 09:07:02 +00:00
import eu.siacs.conversations.ui.util.AvatarWorkerTask ;
2018-03-18 22:32:30 +00:00
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil ;
2018-03-04 10:43:58 +00:00
import eu.siacs.conversations.ui.util.PendingItem ;
2018-04-25 18:25:28 +00:00
import eu.siacs.conversations.ui.util.SoftKeyboardUtils ;
2020-11-16 11:08:07 +00:00
import eu.siacs.conversations.utils.CharSequenceUtils ;
2014-11-15 23:20:20 +00:00
import eu.siacs.conversations.utils.CryptoHelper ;
2018-11-10 09:19:29 +00:00
import eu.siacs.conversations.utils.Resolver ;
2018-10-09 17:35:33 +00:00
import eu.siacs.conversations.utils.SignupUtils ;
2018-12-03 12:15:06 +00:00
import eu.siacs.conversations.utils.TorServiceUtils ;
2014-08-19 13:06:50 +00:00
import eu.siacs.conversations.utils.UIHelper ;
2016-11-28 14:11:44 +00:00
import eu.siacs.conversations.utils.XmppUri ;
2016-02-09 12:01:17 +00:00
import eu.siacs.conversations.xml.Element ;
2020-08-01 13:03:20 +00:00
import eu.siacs.conversations.xmpp.Jid ;
2015-07-21 12:18:16 +00:00
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated ;
2016-12-03 22:49:00 +00:00
import eu.siacs.conversations.xmpp.OnUpdateBlocklist ;
2016-02-19 19:54:53 +00:00
import eu.siacs.conversations.xmpp.XmppConnection ;
2014-08-23 13:56:30 +00:00
import eu.siacs.conversations.xmpp.XmppConnection.Features ;
2015-10-11 11:11:50 +00:00
import eu.siacs.conversations.xmpp.forms.Data ;
2014-08-15 15:31:24 +00:00
import eu.siacs.conversations.xmpp.pep.Avatar ;
2016-12-03 22:49:00 +00:00
public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate , OnUpdateBlocklist ,
2018-11-09 16:47:36 +00:00
OnKeyStatusUpdated , OnCaptchaRequested , KeyChainAliasCallback , XmppConnectionService . OnShowErrorToast , XmppConnectionService . OnMamPreferencesFetched {
public static final String EXTRA_OPENED_FROM_NOTIFICATION = " opened_from_notification " ;
2019-04-25 17:00:59 +00:00
public static final String EXTRA_FORCE_REGISTER = " force_register " ;
2018-11-09 16:47:36 +00:00
private static final int REQUEST_DATA_SAVER = 0xf244 ;
private static final int REQUEST_CHANGE_STATUS = 0xee11 ;
2018-12-03 12:15:06 +00:00
private static final int REQUEST_ORBOT = 0xff22 ;
2018-11-09 16:47:36 +00:00
private final PendingItem < PresenceTemplate > mPendingPresenceTemplate = new PendingItem < > ( ) ;
private AlertDialog mCaptchaDialog = null ;
private Jid jidToEdit ;
private boolean mInitMode = false ;
2019-04-25 17:00:59 +00:00
private Boolean mForceRegister = null ;
2018-11-09 16:47:36 +00:00
private boolean mUsernameMode = Config . DOMAIN_LOCK ! = null ;
private boolean mShowOptions = false ;
private Account mAccount ;
private final OnClickListener mCancelButtonClickListener = v - > {
deleteAccountAndReturnIfNecessary ( ) ;
finish ( ) ;
} ;
private final UiCallback < Avatar > mAvatarFetchCallback = new UiCallback < Avatar > ( ) {
@Override
2019-06-26 15:40:05 +00:00
public void userInputRequired ( final PendingIntent pi , final Avatar avatar ) {
2018-11-09 16:47:36 +00:00
finishInitialSetup ( avatar ) ;
}
@Override
public void success ( final Avatar avatar ) {
finishInitialSetup ( avatar ) ;
}
@Override
public void error ( final int errorCode , final Avatar avatar ) {
finishInitialSetup ( avatar ) ;
}
} ;
private final OnClickListener mAvatarClickListener = new OnClickListener ( ) {
@Override
public void onClick ( final View view ) {
if ( mAccount ! = null ) {
final Intent intent = new Intent ( getApplicationContext ( ) , PublishProfilePictureActivity . class ) ;
2020-05-20 18:14:13 +00:00
intent . putExtra ( EXTRA_ACCOUNT , mAccount . getJid ( ) . asBareJid ( ) . toEscapedString ( ) ) ;
2018-11-09 16:47:36 +00:00
startActivity ( intent ) ;
}
}
} ;
private String messageFingerprint ;
private boolean mFetchingAvatar = false ;
private Toast mFetchingMamPrefsToast ;
private String mSavedInstanceAccount ;
private boolean mSavedInstanceInit = false ;
private XmppUri pendingUri = null ;
private boolean mUseTor ;
private ActivityEditAccountBinding binding ;
private final OnClickListener mSaveButtonClickListener = new OnClickListener ( ) {
@Override
public void onClick ( final View v ) {
final String password = binding . accountPassword . getText ( ) . toString ( ) ;
final boolean wasDisabled = mAccount ! = null & & mAccount . getStatus ( ) = = Account . State . DISABLED ;
final boolean accountInfoEdited = accountInfoEdited ( ) ;
if ( mInitMode & & mAccount ! = null ) {
mAccount . setOption ( Account . OPTION_DISABLED , false ) ;
}
if ( mAccount ! = null & & mAccount . getStatus ( ) = = Account . State . DISABLED & & ! accountInfoEdited ) {
mAccount . setOption ( Account . OPTION_DISABLED , false ) ;
if ( ! xmppConnectionService . updateAccount ( mAccount ) ) {
Toast . makeText ( EditAccountActivity . this , R . string . unable_to_update_account , Toast . LENGTH_SHORT ) . show ( ) ;
}
return ;
}
2019-04-25 17:00:59 +00:00
final boolean registerNewAccount ;
if ( mForceRegister ! = null ) {
registerNewAccount = mForceRegister ;
} else {
registerNewAccount = binding . accountRegisterNew . isChecked ( ) & & ! Config . DISALLOW_REGISTRATION_IN_UI ;
}
2018-11-09 16:47:36 +00:00
if ( mUsernameMode & & binding . accountJid . getText ( ) . toString ( ) . contains ( " @ " ) ) {
binding . accountJidLayout . setError ( getString ( R . string . invalid_username ) ) ;
removeErrorsOnAllBut ( binding . accountJidLayout ) ;
binding . accountJid . requestFocus ( ) ;
return ;
}
XmppConnection connection = mAccount = = null ? null : mAccount . getXmppConnection ( ) ;
2018-12-03 12:15:06 +00:00
final boolean startOrbot = mAccount ! = null & & mAccount . getStatus ( ) = = Account . State . TOR_NOT_AVAILABLE ;
if ( startOrbot ) {
if ( TorServiceUtils . isOrbotInstalled ( EditAccountActivity . this ) ) {
TorServiceUtils . startOrbot ( EditAccountActivity . this , REQUEST_ORBOT ) ;
} else {
TorServiceUtils . downloadOrbot ( EditAccountActivity . this , REQUEST_ORBOT ) ;
}
return ;
}
2018-12-16 22:12:04 +00:00
if ( inNeedOfSaslAccept ( ) ) {
mAccount . setKey ( Account . PINNED_MECHANISM_KEY , String . valueOf ( - 1 ) ) ;
if ( ! xmppConnectionService . updateAccount ( mAccount ) ) {
Toast . makeText ( EditAccountActivity . this , R . string . unable_to_update_account , Toast . LENGTH_SHORT ) . show ( ) ;
}
return ;
}
2018-12-03 12:15:06 +00:00
final boolean openRegistrationUrl = registerNewAccount & & ! accountInfoEdited & & mAccount ! = null & & mAccount . getStatus ( ) = = Account . State . REGISTRATION_WEB ;
final boolean openPaymentUrl = mAccount ! = null & & mAccount . getStatus ( ) = = Account . State . PAYMENT_REQUIRED ;
2018-11-09 16:47:36 +00:00
final boolean redirectionWorthyStatus = openPaymentUrl | | openRegistrationUrl ;
URL url = connection ! = null & & redirectionWorthyStatus ? connection . getRedirectionUrl ( ) : null ;
if ( url ! = null & & ! wasDisabled ) {
try {
startActivity ( new Intent ( Intent . ACTION_VIEW , Uri . parse ( url . toString ( ) ) ) ) ;
return ;
} catch ( ActivityNotFoundException e ) {
Toast . makeText ( EditAccountActivity . this , R . string . application_found_to_open_website , Toast . LENGTH_SHORT ) . show ( ) ;
return ;
}
}
final Jid jid ;
try {
if ( mUsernameMode ) {
2020-05-15 16:21:45 +00:00
jid = Jid . ofEscaped ( binding . accountJid . getText ( ) . toString ( ) , getUserModeDomain ( ) , null ) ;
2018-11-09 16:47:36 +00:00
} else {
2020-05-15 16:21:45 +00:00
jid = Jid . ofEscaped ( binding . accountJid . getText ( ) . toString ( ) ) ;
2018-11-09 16:47:36 +00:00
}
} catch ( final NullPointerException | IllegalArgumentException e ) {
if ( mUsernameMode ) {
binding . accountJidLayout . setError ( getString ( R . string . invalid_username ) ) ;
} else {
binding . accountJidLayout . setError ( getString ( R . string . invalid_jid ) ) ;
}
binding . accountJid . requestFocus ( ) ;
removeErrorsOnAllBut ( binding . accountJidLayout ) ;
return ;
}
2020-08-01 13:03:20 +00:00
final String hostname ;
2018-11-09 16:47:36 +00:00
int numericPort = 5222 ;
if ( mShowOptions ) {
2020-08-01 13:03:20 +00:00
hostname = CharMatcher . whitespace ( ) . removeFrom ( binding . hostname . getText ( ) ) ;
final String port = CharMatcher . whitespace ( ) . removeFrom ( binding . port . getText ( ) ) ;
if ( Resolver . invalidHostname ( hostname ) ) {
2018-11-09 16:47:36 +00:00
binding . hostnameLayout . setError ( getString ( R . string . not_valid_hostname ) ) ;
binding . hostname . requestFocus ( ) ;
removeErrorsOnAllBut ( binding . hostnameLayout ) ;
return ;
}
2019-11-03 21:03:46 +00:00
if ( ! hostname . isEmpty ( ) ) {
try {
numericPort = Integer . parseInt ( port ) ;
if ( numericPort < 0 | | numericPort > 65535 ) {
binding . portLayout . setError ( getString ( R . string . not_a_valid_port ) ) ;
removeErrorsOnAllBut ( binding . portLayout ) ;
binding . port . requestFocus ( ) ;
return ;
}
} catch ( NumberFormatException e ) {
2018-11-09 16:47:36 +00:00
binding . portLayout . setError ( getString ( R . string . not_a_valid_port ) ) ;
removeErrorsOnAllBut ( binding . portLayout ) ;
binding . port . requestFocus ( ) ;
return ;
}
}
2020-08-01 13:03:20 +00:00
} else {
hostname = null ;
2018-11-09 16:47:36 +00:00
}
if ( jid . getLocal ( ) = = null ) {
if ( mUsernameMode ) {
binding . accountJidLayout . setError ( getString ( R . string . invalid_username ) ) ;
} else {
binding . accountJidLayout . setError ( getString ( R . string . invalid_jid ) ) ;
}
removeErrorsOnAllBut ( binding . accountJidLayout ) ;
binding . accountJid . requestFocus ( ) ;
return ;
}
if ( mAccount ! = null ) {
2018-11-22 11:31:06 +00:00
if ( mAccount . isOptionSet ( Account . OPTION_MAGIC_CREATE ) ) {
2018-11-09 16:47:36 +00:00
mAccount . setOption ( Account . OPTION_MAGIC_CREATE , mAccount . getPassword ( ) . contains ( password ) ) ;
}
mAccount . setJid ( jid ) ;
mAccount . setPort ( numericPort ) ;
mAccount . setHostname ( hostname ) ;
binding . accountJidLayout . setError ( null ) ;
mAccount . setPassword ( password ) ;
mAccount . setOption ( Account . OPTION_REGISTER , registerNewAccount ) ;
if ( ! xmppConnectionService . updateAccount ( mAccount ) ) {
Toast . makeText ( EditAccountActivity . this , R . string . unable_to_update_account , Toast . LENGTH_SHORT ) . show ( ) ;
return ;
}
} else {
if ( xmppConnectionService . findAccountByJid ( jid ) ! = null ) {
binding . accountJidLayout . setError ( getString ( R . string . account_already_exists ) ) ;
removeErrorsOnAllBut ( binding . accountJidLayout ) ;
binding . accountJid . requestFocus ( ) ;
return ;
}
mAccount = new Account ( jid . asBareJid ( ) , password ) ;
mAccount . setPort ( numericPort ) ;
mAccount . setHostname ( hostname ) ;
mAccount . setOption ( Account . OPTION_USETLS , true ) ;
mAccount . setOption ( Account . OPTION_USECOMPRESSION , true ) ;
mAccount . setOption ( Account . OPTION_REGISTER , registerNewAccount ) ;
xmppConnectionService . createAccount ( mAccount ) ;
}
binding . hostnameLayout . setError ( null ) ;
binding . portLayout . setError ( null ) ;
if ( mAccount . isEnabled ( )
& & ! registerNewAccount
& & ! mInitMode ) {
finish ( ) ;
} else {
updateSaveButton ( ) ;
updateAccountInformation ( true ) ;
}
}
} ;
private final TextWatcher mTextWatcher = new TextWatcher ( ) {
@Override
public void onTextChanged ( final CharSequence s , final int start , final int before , final int count ) {
updatePortLayout ( ) ;
updateSaveButton ( ) ;
}
@Override
public void beforeTextChanged ( final CharSequence s , final int start , final int count , final int after ) {
}
@Override
public void afterTextChanged ( final Editable s ) {
}
} ;
private View . OnFocusChangeListener mEditTextFocusListener = new View . OnFocusChangeListener ( ) {
@Override
public void onFocusChange ( View view , boolean b ) {
EditText et = ( EditText ) view ;
if ( b ) {
int resId = mUsernameMode ? R . string . username : R . string . account_settings_example_jabber_id ;
if ( view . getId ( ) = = R . id . hostname ) {
resId = mUseTor ? R . string . hostname_or_onion : R . string . hostname_example ;
}
final int res = resId ;
new Handler ( ) . postDelayed ( ( ) - > et . setHint ( res ) , 200 ) ;
} else {
et . setHint ( null ) ;
}
}
} ;
private static void setAvailabilityRadioButton ( Presence . Status status , DialogPresenceBinding binding ) {
if ( status = = null ) {
binding . online . setChecked ( true ) ;
return ;
}
switch ( status ) {
case DND :
binding . dnd . setChecked ( true ) ;
break ;
case XA :
binding . xa . setChecked ( true ) ;
break ;
case AWAY :
2019-01-25 13:39:36 +00:00
binding . away . setChecked ( true ) ;
2018-11-09 16:47:36 +00:00
break ;
default :
binding . online . setChecked ( true ) ;
}
}
private static Presence . Status getAvailabilityRadioButton ( DialogPresenceBinding binding ) {
if ( binding . dnd . isChecked ( ) ) {
return Presence . Status . DND ;
} else if ( binding . xa . isChecked ( ) ) {
return Presence . Status . XA ;
} else if ( binding . away . isChecked ( ) ) {
return Presence . Status . AWAY ;
} else {
return Presence . Status . ONLINE ;
}
}
public void refreshUiReal ( ) {
invalidateOptionsMenu ( ) ;
if ( mAccount ! = null
& & mAccount . getStatus ( ) ! = Account . State . ONLINE
& & mFetchingAvatar ) {
Intent intent = new Intent ( this , StartConversationActivity . class ) ;
StartConversationActivity . addInviteUri ( intent , getIntent ( ) ) ;
startActivity ( intent ) ;
finish ( ) ;
} else if ( mInitMode & & mAccount ! = null & & mAccount . getStatus ( ) = = Account . State . ONLINE ) {
if ( ! mFetchingAvatar ) {
mFetchingAvatar = true ;
xmppConnectionService . checkForAvatar ( mAccount , mAvatarFetchCallback ) ;
}
}
if ( mAccount ! = null ) {
updateAccountInformation ( false ) ;
}
updateSaveButton ( ) ;
}
@Override
public boolean onNavigateUp ( ) {
deleteAccountAndReturnIfNecessary ( ) ;
return super . onNavigateUp ( ) ;
}
@Override
public void onBackPressed ( ) {
deleteAccountAndReturnIfNecessary ( ) ;
super . onBackPressed ( ) ;
}
private void deleteAccountAndReturnIfNecessary ( ) {
if ( mInitMode & & mAccount ! = null & & ! mAccount . isOptionSet ( Account . OPTION_LOGGED_IN_SUCCESSFULLY ) ) {
xmppConnectionService . deleteAccount ( mAccount ) ;
}
2020-01-09 13:13:05 +00:00
final boolean magicCreate = mAccount ! = null & & mAccount . isOptionSet ( Account . OPTION_MAGIC_CREATE ) & & ! mAccount . isOptionSet ( Account . OPTION_LOGGED_IN_SUCCESSFULLY ) ;
final Jid jid = mAccount = = null ? null : mAccount . getJid ( ) ;
if ( SignupUtils . isSupportTokenRegistry ( ) & & jid ! = null & & magicCreate & & ! jid . getDomain ( ) . equals ( Config . MAGIC_CREATE_DOMAIN ) ) {
2020-01-09 19:10:19 +00:00
final Jid preset ;
if ( mAccount . isOptionSet ( Account . OPTION_FIXED_USERNAME ) ) {
preset = jid . asBareJid ( ) ;
} else {
2020-05-21 05:57:57 +00:00
preset = jid . getDomain ( ) ;
2020-01-09 19:10:19 +00:00
}
final Intent intent = SignupUtils . getTokenRegistrationIntent ( this , preset , mAccount . getKey ( Account . PRE_AUTH_REGISTRATION_TOKEN ) ) ;
StartConversationActivity . addInviteUri ( intent , getIntent ( ) ) ;
2020-01-09 13:13:05 +00:00
startActivity ( intent ) ;
return ;
}
2020-03-31 17:46:05 +00:00
final List < Account > accounts = xmppConnectionService = = null ? null : xmppConnectionService . getAccounts ( ) ;
if ( accounts ! = null & & accounts . size ( ) = = 0 & & Config . MAGIC_CREATE_DOMAIN ! = null ) {
2019-04-25 17:00:59 +00:00
Intent intent = SignupUtils . getSignUpIntent ( this , mForceRegister ! = null & & mForceRegister ) ;
2020-01-09 19:10:19 +00:00
StartConversationActivity . addInviteUri ( intent , getIntent ( ) ) ;
2018-11-09 16:47:36 +00:00
startActivity ( intent ) ;
}
}
@Override
public void onAccountUpdate ( ) {
refreshUi ( ) ;
}
protected void finishInitialSetup ( final Avatar avatar ) {
runOnUiThread ( ( ) - > {
SoftKeyboardUtils . hideSoftKeyboard ( EditAccountActivity . this ) ;
final Intent intent ;
final XmppConnection connection = mAccount . getXmppConnection ( ) ;
final boolean wasFirstAccount = xmppConnectionService ! = null & & xmppConnectionService . getAccounts ( ) . size ( ) = = 1 ;
if ( avatar ! = null | | ( connection ! = null & & ! connection . getFeatures ( ) . pep ( ) ) ) {
intent = new Intent ( getApplicationContext ( ) , StartConversationActivity . class ) ;
if ( wasFirstAccount ) {
intent . putExtra ( " init " , true ) ;
}
2020-01-09 16:39:55 +00:00
intent . putExtra ( EXTRA_ACCOUNT , mAccount . getJid ( ) . asBareJid ( ) . toEscapedString ( ) ) ;
2018-11-09 16:47:36 +00:00
} else {
intent = new Intent ( getApplicationContext ( ) , PublishProfilePictureActivity . class ) ;
2020-05-20 18:14:13 +00:00
intent . putExtra ( EXTRA_ACCOUNT , mAccount . getJid ( ) . asBareJid ( ) . toEscapedString ( ) ) ;
2018-11-09 16:47:36 +00:00
intent . putExtra ( " setup " , true ) ;
}
if ( wasFirstAccount ) {
intent . setFlags ( Intent . FLAG_ACTIVITY_NEW_TASK | Intent . FLAG_ACTIVITY_CLEAR_TASK ) ;
}
StartConversationActivity . addInviteUri ( intent , getIntent ( ) ) ;
startActivity ( intent ) ;
finish ( ) ;
} ) ;
}
@Override
public void onActivityResult ( int requestCode , int resultCode , Intent data ) {
super . onActivityResult ( requestCode , resultCode , data ) ;
if ( requestCode = = REQUEST_BATTERY_OP | | requestCode = = REQUEST_DATA_SAVER ) {
updateAccountInformation ( mAccount = = null ) ;
}
if ( requestCode = = REQUEST_CHANGE_STATUS ) {
PresenceTemplate template = mPendingPresenceTemplate . pop ( ) ;
if ( template ! = null & & resultCode = = Activity . RESULT_OK ) {
generateSignature ( data , template ) ;
} else {
Log . d ( Config . LOGTAG , " pgp result not ok " ) ;
}
}
}
@Override
protected void processFingerprintVerification ( XmppUri uri ) {
processFingerprintVerification ( uri , true ) ;
}
protected void processFingerprintVerification ( XmppUri uri , boolean showWarningToast ) {
if ( mAccount ! = null & & mAccount . getJid ( ) . asBareJid ( ) . equals ( uri . getJid ( ) ) & & uri . hasFingerprints ( ) ) {
if ( xmppConnectionService . verifyFingerprints ( mAccount , uri . getFingerprints ( ) ) ) {
Toast . makeText ( this , R . string . verified_fingerprints , Toast . LENGTH_SHORT ) . show ( ) ;
updateAccountInformation ( false ) ;
}
} else if ( showWarningToast ) {
Toast . makeText ( this , R . string . invalid_barcode , Toast . LENGTH_SHORT ) . show ( ) ;
}
}
private void updatePortLayout ( ) {
2019-11-03 21:03:46 +00:00
final String hostname = this . binding . hostname . getText ( ) . toString ( ) ;
if ( TextUtils . isEmpty ( hostname ) ) {
this . binding . portLayout . setEnabled ( false ) ;
this . binding . portLayout . setError ( null ) ;
} else {
this . binding . portLayout . setEnabled ( true ) ;
}
2018-11-09 16:47:36 +00:00
}
protected void updateSaveButton ( ) {
boolean accountInfoEdited = accountInfoEdited ( ) ;
2018-11-21 10:55:29 +00:00
if ( accountInfoEdited & & ! mInitMode ) {
2018-11-09 16:47:36 +00:00
this . binding . saveButton . setText ( R . string . save ) ;
this . binding . saveButton . setEnabled ( true ) ;
} else if ( mAccount ! = null
& & ( mAccount . getStatus ( ) = = Account . State . CONNECTING | | mAccount . getStatus ( ) = = Account . State . REGISTRATION_SUCCESSFUL | | mFetchingAvatar ) ) {
this . binding . saveButton . setEnabled ( false ) ;
this . binding . saveButton . setText ( R . string . account_status_connecting ) ;
} else if ( mAccount ! = null & & mAccount . getStatus ( ) = = Account . State . DISABLED & & ! mInitMode ) {
this . binding . saveButton . setEnabled ( true ) ;
this . binding . saveButton . setText ( R . string . enable ) ;
2018-12-03 12:15:06 +00:00
} else if ( torNeedsInstall ( mAccount ) ) {
this . binding . saveButton . setEnabled ( true ) ;
this . binding . saveButton . setText ( R . string . install_orbot ) ;
} else if ( torNeedsStart ( mAccount ) ) {
this . binding . saveButton . setEnabled ( true ) ;
this . binding . saveButton . setText ( R . string . start_orbot ) ;
2018-11-09 16:47:36 +00:00
} else {
this . binding . saveButton . setEnabled ( true ) ;
if ( ! mInitMode ) {
if ( mAccount ! = null & & mAccount . isOnlineAndConnected ( ) ) {
this . binding . saveButton . setText ( R . string . save ) ;
if ( ! accountInfoEdited ) {
this . binding . saveButton . setEnabled ( false ) ;
}
} else {
XmppConnection connection = mAccount = = null ? null : mAccount . getXmppConnection ( ) ;
URL url = connection ! = null & & mAccount . getStatus ( ) = = Account . State . PAYMENT_REQUIRED ? connection . getRedirectionUrl ( ) : null ;
if ( url ! = null ) {
this . binding . saveButton . setText ( R . string . open_website ) ;
2018-12-16 22:12:04 +00:00
} else if ( inNeedOfSaslAccept ( ) ) {
this . binding . saveButton . setText ( R . string . accept ) ;
2018-11-09 16:47:36 +00:00
} else {
this . binding . saveButton . setText ( R . string . connect ) ;
}
}
} else {
XmppConnection connection = mAccount = = null ? null : mAccount . getXmppConnection ( ) ;
URL url = connection ! = null & & mAccount . getStatus ( ) = = Account . State . REGISTRATION_WEB ? connection . getRedirectionUrl ( ) : null ;
if ( url ! = null & & this . binding . accountRegisterNew . isChecked ( ) & & ! accountInfoEdited ) {
this . binding . saveButton . setText ( R . string . open_website ) ;
} else {
this . binding . saveButton . setText ( R . string . next ) ;
}
}
}
}
2018-12-03 12:15:06 +00:00
private boolean torNeedsInstall ( final Account account ) {
return account ! = null & & account . getStatus ( ) = = Account . State . TOR_NOT_AVAILABLE & & ! TorServiceUtils . isOrbotInstalled ( this ) ;
}
private boolean torNeedsStart ( final Account account ) {
return account ! = null & & account . getStatus ( ) = = Account . State . TOR_NOT_AVAILABLE ;
}
2018-11-09 16:47:36 +00:00
protected boolean accountInfoEdited ( ) {
if ( this . mAccount = = null ) {
return false ;
}
return jidEdited ( ) | |
! this . mAccount . getPassword ( ) . equals ( this . binding . accountPassword . getText ( ) . toString ( ) ) | |
! this . mAccount . getHostname ( ) . equals ( this . binding . hostname . getText ( ) . toString ( ) ) | |
! String . valueOf ( this . mAccount . getPort ( ) ) . equals ( this . binding . port . getText ( ) . toString ( ) ) ;
}
protected boolean jidEdited ( ) {
final String unmodified ;
if ( mUsernameMode ) {
2020-05-15 18:20:22 +00:00
unmodified = this . mAccount . getJid ( ) . getEscapedLocal ( ) ;
2018-11-09 16:47:36 +00:00
} else {
2020-05-15 18:20:22 +00:00
unmodified = this . mAccount . getJid ( ) . asBareJid ( ) . toEscapedString ( ) ;
2018-11-09 16:47:36 +00:00
}
return ! unmodified . equals ( this . binding . accountJid . getText ( ) . toString ( ) ) ;
}
@Override
protected String getShareableUri ( boolean http ) {
if ( mAccount ! = null ) {
return http ? mAccount . getShareableLink ( ) : mAccount . getShareableUri ( ) ;
} else {
return null ;
}
}
@Override
protected void onCreate ( final Bundle savedInstanceState ) {
super . onCreate ( savedInstanceState ) ;
if ( savedInstanceState ! = null ) {
this . mSavedInstanceAccount = savedInstanceState . getString ( " account " ) ;
this . mSavedInstanceInit = savedInstanceState . getBoolean ( " initMode " , false ) ;
}
this . binding = DataBindingUtil . setContentView ( this , R . layout . activity_edit_account ) ;
setSupportActionBar ( ( Toolbar ) binding . toolbar ) ;
binding . accountJid . addTextChangedListener ( this . mTextWatcher ) ;
binding . accountJid . setOnFocusChangeListener ( this . mEditTextFocusListener ) ;
this . binding . accountPassword . addTextChangedListener ( this . mTextWatcher ) ;
this . binding . avater . setOnClickListener ( this . mAvatarClickListener ) ;
this . binding . hostname . addTextChangedListener ( mTextWatcher ) ;
this . binding . hostname . setOnFocusChangeListener ( mEditTextFocusListener ) ;
this . binding . clearDevices . setOnClickListener ( v - > showWipePepDialog ( ) ) ;
2018-11-10 09:19:29 +00:00
this . binding . port . setText ( String . valueOf ( Resolver . DEFAULT_PORT_XMPP ) ) ;
2018-11-09 16:47:36 +00:00
this . binding . port . addTextChangedListener ( mTextWatcher ) ;
this . binding . saveButton . setOnClickListener ( this . mSaveButtonClickListener ) ;
this . binding . cancelButton . setOnClickListener ( this . mCancelButtonClickListener ) ;
if ( savedInstanceState ! = null & & savedInstanceState . getBoolean ( " showMoreTable " ) ) {
changeMoreTableVisibility ( true ) ;
}
final OnCheckedChangeListener OnCheckedShowConfirmPassword = ( buttonView , isChecked ) - > updateSaveButton ( ) ;
this . binding . accountRegisterNew . setOnCheckedChangeListener ( OnCheckedShowConfirmPassword ) ;
if ( Config . DISALLOW_REGISTRATION_IN_UI ) {
this . binding . accountRegisterNew . setVisibility ( View . GONE ) ;
}
this . binding . actionEditYourName . setOnClickListener ( this : : onEditYourNameClicked ) ;
}
private void onEditYourNameClicked ( View view ) {
quickEdit ( mAccount . getDisplayName ( ) , R . string . your_name , value - > {
final String displayName = value . trim ( ) ;
updateDisplayName ( displayName ) ;
mAccount . setDisplayName ( displayName ) ;
xmppConnectionService . publishDisplayName ( mAccount ) ;
2018-11-11 09:13:45 +00:00
refreshAvatar ( ) ;
2018-11-09 16:47:36 +00:00
return null ;
} , true ) ;
}
2018-11-11 09:13:45 +00:00
private void refreshAvatar ( ) {
2019-11-03 21:03:46 +00:00
AvatarWorkerTask . loadAvatar ( mAccount , binding . avater , R . dimen . avatar_on_details_screen_size ) ;
2018-11-11 09:13:45 +00:00
}
2018-11-09 16:47:36 +00:00
@Override
public boolean onCreateOptionsMenu ( final Menu menu ) {
super . onCreateOptionsMenu ( menu ) ;
getMenuInflater ( ) . inflate ( R . menu . editaccount , menu ) ;
final MenuItem showBlocklist = menu . findItem ( R . id . action_show_block_list ) ;
final MenuItem showMoreInfo = menu . findItem ( R . id . action_server_info_show_more ) ;
final MenuItem changePassword = menu . findItem ( R . id . action_change_password_on_server ) ;
final MenuItem renewCertificate = menu . findItem ( R . id . action_renew_certificate ) ;
final MenuItem mamPrefs = menu . findItem ( R . id . action_mam_prefs ) ;
final MenuItem changePresence = menu . findItem ( R . id . action_change_presence ) ;
final MenuItem share = menu . findItem ( R . id . action_share ) ;
renewCertificate . setVisible ( mAccount ! = null & & mAccount . getPrivateKeyAlias ( ) ! = null ) ;
share . setVisible ( mAccount ! = null & & ! mInitMode ) ;
if ( mAccount ! = null & & mAccount . isOnlineAndConnected ( ) ) {
if ( ! mAccount . getXmppConnection ( ) . getFeatures ( ) . blocking ( ) ) {
showBlocklist . setVisible ( false ) ;
}
if ( ! mAccount . getXmppConnection ( ) . getFeatures ( ) . register ( ) ) {
changePassword . setVisible ( false ) ;
}
mamPrefs . setVisible ( mAccount . getXmppConnection ( ) . getFeatures ( ) . mam ( ) ) ;
changePresence . setVisible ( ! mInitMode ) ;
} else {
showBlocklist . setVisible ( false ) ;
showMoreInfo . setVisible ( false ) ;
changePassword . setVisible ( false ) ;
mamPrefs . setVisible ( false ) ;
changePresence . setVisible ( false ) ;
}
return super . onCreateOptionsMenu ( menu ) ;
}
@Override
public boolean onPrepareOptionsMenu ( Menu menu ) {
final MenuItem showMoreInfo = menu . findItem ( R . id . action_server_info_show_more ) ;
if ( showMoreInfo . isVisible ( ) ) {
showMoreInfo . setChecked ( binding . serverInfoMore . getVisibility ( ) = = View . VISIBLE ) ;
}
return super . onPrepareOptionsMenu ( menu ) ;
}
@Override
protected void onStart ( ) {
super . onStart ( ) ;
final Intent intent = getIntent ( ) ;
final int theme = findTheme ( ) ;
if ( this . mTheme ! = theme ) {
recreate ( ) ;
} else if ( intent ! = null ) {
try {
2020-05-20 18:14:13 +00:00
this . jidToEdit = Jid . ofEscaped ( intent . getStringExtra ( " jid " ) ) ;
2018-11-09 16:47:36 +00:00
} catch ( final IllegalArgumentException | NullPointerException ignored ) {
this . jidToEdit = null ;
}
if ( jidToEdit ! = null & & intent . getData ( ) ! = null & & intent . getBooleanExtra ( " scanned " , false ) ) {
final XmppUri uri = new XmppUri ( intent . getData ( ) ) ;
if ( xmppConnectionServiceBound ) {
processFingerprintVerification ( uri , false ) ;
} else {
this . pendingUri = uri ;
}
}
boolean init = intent . getBooleanExtra ( " init " , false ) ;
boolean openedFromNotification = intent . getBooleanExtra ( EXTRA_OPENED_FROM_NOTIFICATION , false ) ;
2019-11-03 21:03:46 +00:00
Log . d ( Config . LOGTAG , " extras " + intent . getExtras ( ) ) ;
this . mForceRegister = intent . hasExtra ( EXTRA_FORCE_REGISTER ) ? intent . getBooleanExtra ( EXTRA_FORCE_REGISTER , false ) : null ;
Log . d ( Config . LOGTAG , " force register= " + mForceRegister ) ;
2018-11-09 16:47:36 +00:00
this . mInitMode = init | | this . jidToEdit = = null ;
this . messageFingerprint = intent . getStringExtra ( " fingerprint " ) ;
if ( ! mInitMode ) {
this . binding . accountRegisterNew . setVisibility ( View . GONE ) ;
setTitle ( getString ( R . string . account_details ) ) ;
configureActionBar ( getSupportActionBar ( ) , ! openedFromNotification ) ;
} else {
this . binding . avater . setVisibility ( View . GONE ) ;
configureActionBar ( getSupportActionBar ( ) , ! ( init & & Config . MAGIC_CREATE_DOMAIN = = null ) ) ;
2019-04-25 17:00:59 +00:00
if ( mForceRegister ! = null ) {
if ( mForceRegister ) {
setTitle ( R . string . register_new_account ) ;
} else {
setTitle ( R . string . add_existing_account ) ;
}
} else {
setTitle ( R . string . action_add_account ) ;
}
2018-11-09 16:47:36 +00:00
}
}
SharedPreferences preferences = getPreferences ( ) ;
mUseTor = QuickConversationsService . isConversations ( ) & & preferences . getBoolean ( " use_tor " , getResources ( ) . getBoolean ( R . bool . use_tor ) ) ;
this . mShowOptions = mUseTor | | ( QuickConversationsService . isConversations ( ) & & preferences . getBoolean ( " show_connection_options " , getResources ( ) . getBoolean ( R . bool . show_connection_options ) ) ) ;
this . binding . namePort . setVisibility ( mShowOptions ? View . VISIBLE : View . GONE ) ;
2019-04-25 17:00:59 +00:00
if ( mForceRegister ! = null ) {
this . binding . accountRegisterNew . setVisibility ( View . GONE ) ;
}
2018-11-09 16:47:36 +00:00
}
@Override
public void onNewIntent ( Intent intent ) {
if ( intent ! = null & & intent . getData ( ) ! = null ) {
final XmppUri uri = new XmppUri ( intent . getData ( ) ) ;
if ( xmppConnectionServiceBound ) {
processFingerprintVerification ( uri , false ) ;
} else {
this . pendingUri = uri ;
}
}
}
@Override
public void onSaveInstanceState ( final Bundle savedInstanceState ) {
if ( mAccount ! = null ) {
2020-05-20 18:14:13 +00:00
savedInstanceState . putString ( " account " , mAccount . getJid ( ) . asBareJid ( ) . toEscapedString ( ) ) ;
2018-11-09 16:47:36 +00:00
savedInstanceState . putBoolean ( " initMode " , mInitMode ) ;
savedInstanceState . putBoolean ( " showMoreTable " , binding . serverInfoMore . getVisibility ( ) = = View . VISIBLE ) ;
}
super . onSaveInstanceState ( savedInstanceState ) ;
}
protected void onBackendConnected ( ) {
boolean init = true ;
if ( mSavedInstanceAccount ! = null ) {
try {
2020-05-20 18:14:13 +00:00
this . mAccount = xmppConnectionService . findAccountByJid ( Jid . ofEscaped ( mSavedInstanceAccount ) ) ;
2018-11-09 16:47:36 +00:00
this . mInitMode = mSavedInstanceInit ;
init = false ;
} catch ( IllegalArgumentException e ) {
this . mAccount = null ;
}
} else if ( this . jidToEdit ! = null ) {
this . mAccount = xmppConnectionService . findAccountByJid ( jidToEdit ) ;
}
if ( mAccount ! = null ) {
this . mInitMode | = this . mAccount . isOptionSet ( Account . OPTION_REGISTER ) ;
this . mUsernameMode | = mAccount . isOptionSet ( Account . OPTION_MAGIC_CREATE ) & & mAccount . isOptionSet ( Account . OPTION_REGISTER ) ;
if ( mPendingFingerprintVerificationUri ! = null ) {
processFingerprintVerification ( mPendingFingerprintVerificationUri , false ) ;
mPendingFingerprintVerificationUri = null ;
}
updateAccountInformation ( init ) ;
}
if ( Config . MAGIC_CREATE_DOMAIN = = null & & this . xmppConnectionService . getAccounts ( ) . size ( ) = = 0 ) {
this . binding . cancelButton . setEnabled ( false ) ;
}
if ( mUsernameMode ) {
this . binding . accountJidLayout . setHint ( getString ( R . string . username_hint ) ) ;
this . binding . accountJid . setHint ( R . string . username_hint ) ;
} else {
final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter ( this ,
R . layout . simple_list_item ,
xmppConnectionService . getKnownHosts ( ) ) ;
this . binding . accountJid . setAdapter ( mKnownHostsAdapter ) ;
}
if ( pendingUri ! = null ) {
processFingerprintVerification ( pendingUri , false ) ;
pendingUri = null ;
}
updatePortLayout ( ) ;
updateSaveButton ( ) ;
invalidateOptionsMenu ( ) ;
}
private String getUserModeDomain ( ) {
if ( mAccount ! = null & & mAccount . getJid ( ) . getDomain ( ) ! = null ) {
2020-05-17 08:24:46 +00:00
return mAccount . getServer ( ) ;
2018-11-09 16:47:36 +00:00
} else {
return Config . DOMAIN_LOCK ;
}
}
@Override
public boolean onOptionsItemSelected ( final MenuItem item ) {
if ( MenuDoubleTabUtil . shouldIgnoreTap ( ) ) {
return false ;
}
switch ( item . getItemId ( ) ) {
2020-01-09 13:13:05 +00:00
case android . R . id . home :
deleteAccountAndReturnIfNecessary ( ) ;
break ;
2018-11-09 16:47:36 +00:00
case R . id . action_show_block_list :
final Intent showBlocklistIntent = new Intent ( this , BlocklistActivity . class ) ;
2020-05-20 18:14:13 +00:00
showBlocklistIntent . putExtra ( EXTRA_ACCOUNT , mAccount . getJid ( ) . toEscapedString ( ) ) ;
2018-11-09 16:47:36 +00:00
startActivity ( showBlocklistIntent ) ;
break ;
case R . id . action_server_info_show_more :
changeMoreTableVisibility ( ! item . isChecked ( ) ) ;
break ;
case R . id . action_share_barcode :
shareBarcode ( ) ;
break ;
case R . id . action_share_http :
shareLink ( true ) ;
break ;
case R . id . action_share_uri :
shareLink ( false ) ;
break ;
case R . id . action_change_password_on_server :
gotoChangePassword ( null ) ;
break ;
case R . id . action_mam_prefs :
editMamPrefs ( ) ;
break ;
case R . id . action_renew_certificate :
renewCertificate ( ) ;
break ;
case R . id . action_change_presence :
changePresence ( ) ;
break ;
}
return super . onOptionsItemSelected ( item ) ;
}
2018-12-16 22:12:04 +00:00
private boolean inNeedOfSaslAccept ( ) {
return mAccount ! = null & & mAccount . getLastErrorStatus ( ) = = Account . State . DOWNGRADE_ATTACK & & mAccount . getKeyAsInt ( Account . PINNED_MECHANISM_KEY , - 1 ) > = 0 & & ! accountInfoEdited ( ) ;
}
2018-11-09 16:47:36 +00:00
private void shareBarcode ( ) {
Intent intent = new Intent ( Intent . ACTION_SEND ) ;
intent . putExtra ( Intent . EXTRA_STREAM , BarcodeProvider . getUriForAccount ( this , mAccount ) ) ;
intent . setFlags ( Intent . FLAG_GRANT_READ_URI_PERMISSION ) ;
intent . setType ( " image/png " ) ;
startActivity ( Intent . createChooser ( intent , getText ( R . string . share_with ) ) ) ;
}
private void changeMoreTableVisibility ( boolean visible ) {
binding . serverInfoMore . setVisibility ( visible ? View . VISIBLE : View . GONE ) ;
}
private void gotoChangePassword ( String newPassword ) {
final Intent changePasswordIntent = new Intent ( this , ChangePasswordActivity . class ) ;
2020-05-20 18:14:13 +00:00
changePasswordIntent . putExtra ( EXTRA_ACCOUNT , mAccount . getJid ( ) . toEscapedString ( ) ) ;
2018-11-09 16:47:36 +00:00
if ( newPassword ! = null ) {
changePasswordIntent . putExtra ( " password " , newPassword ) ;
}
startActivity ( changePasswordIntent ) ;
}
private void renewCertificate ( ) {
KeyChain . choosePrivateKeyAlias ( this , this , null , null , null , - 1 , null ) ;
}
private void changePresence ( ) {
SharedPreferences sharedPreferences = PreferenceManager . getDefaultSharedPreferences ( this ) ;
boolean manualStatus = sharedPreferences . getBoolean ( SettingsActivity . MANUALLY_CHANGE_PRESENCE , getResources ( ) . getBoolean ( R . bool . manually_change_presence ) ) ;
AlertDialog . Builder builder = new AlertDialog . Builder ( this ) ;
final DialogPresenceBinding binding = DataBindingUtil . inflate ( getLayoutInflater ( ) , R . layout . dialog_presence , null , false ) ;
String current = mAccount . getPresenceStatusMessage ( ) ;
if ( current ! = null & & ! current . trim ( ) . isEmpty ( ) ) {
binding . statusMessage . append ( current ) ;
}
setAvailabilityRadioButton ( mAccount . getPresenceStatus ( ) , binding ) ;
binding . show . setVisibility ( manualStatus ? View . VISIBLE : View . GONE ) ;
List < PresenceTemplate > templates = xmppConnectionService . getPresenceTemplates ( mAccount ) ;
PresenceTemplateAdapter presenceTemplateAdapter = new PresenceTemplateAdapter ( this , R . layout . simple_list_item , templates ) ;
binding . statusMessage . setAdapter ( presenceTemplateAdapter ) ;
binding . statusMessage . setOnItemClickListener ( ( parent , view , position , id ) - > {
PresenceTemplate template = ( PresenceTemplate ) parent . getItemAtPosition ( position ) ;
setAvailabilityRadioButton ( template . getStatus ( ) , binding ) ;
} ) ;
builder . setTitle ( R . string . edit_status_message_title ) ;
builder . setView ( binding . getRoot ( ) ) ;
builder . setNegativeButton ( R . string . cancel , null ) ;
builder . setPositiveButton ( R . string . confirm , ( dialog , which ) - > {
PresenceTemplate template = new PresenceTemplate ( getAvailabilityRadioButton ( binding ) , binding . statusMessage . getText ( ) . toString ( ) . trim ( ) ) ;
if ( mAccount . getPgpId ( ) ! = 0 & & hasPgp ( ) ) {
generateSignature ( null , template ) ;
} else {
xmppConnectionService . changeStatus ( mAccount , template , null ) ;
}
} ) ;
builder . create ( ) . show ( ) ;
}
private void generateSignature ( Intent intent , PresenceTemplate template ) {
xmppConnectionService . getPgpEngine ( ) . generateSignature ( intent , mAccount , template . getStatusMessage ( ) , new UiCallback < String > ( ) {
@Override
public void success ( String signature ) {
xmppConnectionService . changeStatus ( mAccount , template , signature ) ;
}
@Override
public void error ( int errorCode , String object ) {
}
@Override
2019-06-26 15:40:05 +00:00
public void userInputRequired ( PendingIntent pi , String object ) {
2018-11-09 16:47:36 +00:00
mPendingPresenceTemplate . push ( template ) ;
try {
startIntentSenderForResult ( pi . getIntentSender ( ) , REQUEST_CHANGE_STATUS , null , 0 , 0 , 0 ) ;
} catch ( final IntentSender . SendIntentException ignored ) {
}
}
} ) ;
}
@Override
public void alias ( String alias ) {
if ( alias ! = null ) {
xmppConnectionService . updateKeyInAccount ( mAccount , alias ) ;
}
}
private void updateAccountInformation ( boolean init ) {
if ( init ) {
this . binding . accountJid . getEditableText ( ) . clear ( ) ;
if ( mUsernameMode ) {
2020-05-15 16:21:45 +00:00
this . binding . accountJid . getEditableText ( ) . append ( this . mAccount . getJid ( ) . getEscapedLocal ( ) ) ;
2018-11-09 16:47:36 +00:00
} else {
2020-05-15 16:21:45 +00:00
this . binding . accountJid . getEditableText ( ) . append ( this . mAccount . getJid ( ) . asBareJid ( ) . toEscapedString ( ) ) ;
2018-11-09 16:47:36 +00:00
}
this . binding . accountPassword . getEditableText ( ) . clear ( ) ;
this . binding . accountPassword . getEditableText ( ) . append ( this . mAccount . getPassword ( ) ) ;
this . binding . hostname . setText ( " " ) ;
this . binding . hostname . getEditableText ( ) . append ( this . mAccount . getHostname ( ) ) ;
this . binding . port . setText ( " " ) ;
this . binding . port . getEditableText ( ) . append ( String . valueOf ( this . mAccount . getPort ( ) ) ) ;
this . binding . namePort . setVisibility ( mShowOptions ? View . VISIBLE : View . GONE ) ;
}
2020-11-16 11:08:07 +00:00
if ( ! mInitMode & & Build . VERSION . SDK_INT > = Build . VERSION_CODES . O ) {
this . binding . accountPassword . setImportantForAutofill ( View . IMPORTANT_FOR_AUTOFILL_NO ) ;
}
2020-01-09 19:10:19 +00:00
final boolean editable = ! mAccount . isOptionSet ( Account . OPTION_LOGGED_IN_SUCCESSFULLY ) & & ! mAccount . isOptionSet ( Account . OPTION_FIXED_USERNAME ) & & QuickConversationsService . isConversations ( ) ;
2018-11-09 16:47:36 +00:00
this . binding . accountJid . setEnabled ( editable ) ;
this . binding . accountJid . setFocusable ( editable ) ;
this . binding . accountJid . setFocusableInTouchMode ( editable ) ;
2018-11-20 20:33:43 +00:00
this . binding . accountJid . setCursorVisible ( editable ) ;
2018-11-09 16:47:36 +00:00
final String displayName = mAccount . getDisplayName ( ) ;
updateDisplayName ( displayName ) ;
2020-11-16 11:08:07 +00:00
final boolean togglePassword = mAccount . isOptionSet ( Account . OPTION_MAGIC_CREATE ) | | ! mAccount . isOptionSet ( Account . OPTION_LOGGED_IN_SUCCESSFULLY ) ;
2018-11-21 15:45:38 +00:00
final boolean editPassword = ! mAccount . isOptionSet ( Account . OPTION_MAGIC_CREATE ) | | ( ! mAccount . isOptionSet ( Account . OPTION_LOGGED_IN_SUCCESSFULLY ) & & QuickConversationsService . isConversations ( ) ) | | mAccount . getLastErrorStatus ( ) = = Account . State . UNAUTHORIZED ;
2018-11-20 20:33:43 +00:00
2020-11-16 11:08:07 +00:00
this . binding . accountPasswordLayout . setPasswordVisibilityToggleEnabled ( togglePassword ) ;
2018-11-20 20:33:43 +00:00
this . binding . accountPassword . setFocusable ( editPassword ) ;
this . binding . accountPassword . setFocusableInTouchMode ( editPassword ) ;
this . binding . accountPassword . setCursorVisible ( editPassword ) ;
this . binding . accountPassword . setEnabled ( editPassword ) ;
2018-11-09 16:47:36 +00:00
if ( ! mInitMode ) {
this . binding . avater . setVisibility ( View . VISIBLE ) ;
2019-11-03 21:03:46 +00:00
AvatarWorkerTask . loadAvatar ( mAccount , binding . avater , R . dimen . avatar_on_details_screen_size ) ;
2018-11-09 16:47:36 +00:00
} else {
this . binding . avater . setVisibility ( View . GONE ) ;
}
this . binding . accountRegisterNew . setChecked ( this . mAccount . isOptionSet ( Account . OPTION_REGISTER ) ) ;
if ( this . mAccount . isOptionSet ( Account . OPTION_MAGIC_CREATE ) ) {
if ( this . mAccount . isOptionSet ( Account . OPTION_REGISTER ) ) {
ActionBar actionBar = getSupportActionBar ( ) ;
if ( actionBar ! = null ) {
actionBar . setTitle ( R . string . create_account ) ;
}
}
this . binding . accountRegisterNew . setVisibility ( View . GONE ) ;
2019-04-25 17:00:59 +00:00
} else if ( this . mAccount . isOptionSet ( Account . OPTION_REGISTER ) & & mForceRegister = = null ) {
2018-11-09 16:47:36 +00:00
this . binding . accountRegisterNew . setVisibility ( View . VISIBLE ) ;
} else {
this . binding . accountRegisterNew . setVisibility ( View . GONE ) ;
}
if ( this . mAccount . isOnlineAndConnected ( ) & & ! this . mFetchingAvatar ) {
Features features = this . mAccount . getXmppConnection ( ) . getFeatures ( ) ;
this . binding . stats . setVisibility ( View . VISIBLE ) ;
boolean showBatteryWarning = ! xmppConnectionService . getPushManagementService ( ) . available ( mAccount ) & & isOptimizingBattery ( ) ;
boolean showDataSaverWarning = isAffectedByDataSaver ( ) ;
showOsOptimizationWarning ( showBatteryWarning , showDataSaverWarning ) ;
this . binding . sessionEst . setText ( UIHelper . readableTimeDifferenceFull ( this , this . mAccount . getXmppConnection ( )
. getLastSessionEstablished ( ) ) ) ;
if ( features . rosterVersioning ( ) ) {
this . binding . serverInfoRosterVersion . setText ( R . string . server_info_available ) ;
} else {
this . binding . serverInfoRosterVersion . setText ( R . string . server_info_unavailable ) ;
}
if ( features . carbons ( ) ) {
this . binding . serverInfoCarbons . setText ( R . string . server_info_available ) ;
} else {
this . binding . serverInfoCarbons . setText ( R . string . server_info_unavailable ) ;
}
if ( features . mam ( ) ) {
this . binding . serverInfoMam . setText ( R . string . server_info_available ) ;
} else {
this . binding . serverInfoMam . setText ( R . string . server_info_unavailable ) ;
}
if ( features . csi ( ) ) {
this . binding . serverInfoCsi . setText ( R . string . server_info_available ) ;
} else {
this . binding . serverInfoCsi . setText ( R . string . server_info_unavailable ) ;
}
if ( features . blocking ( ) ) {
this . binding . serverInfoBlocking . setText ( R . string . server_info_available ) ;
} else {
this . binding . serverInfoBlocking . setText ( R . string . server_info_unavailable ) ;
}
if ( features . sm ( ) ) {
this . binding . serverInfoSm . setText ( R . string . server_info_available ) ;
} else {
this . binding . serverInfoSm . setText ( R . string . server_info_unavailable ) ;
}
2020-04-21 09:40:05 +00:00
if ( features . externalServiceDiscovery ( ) ) {
this . binding . serverInfoExternalService . setText ( R . string . server_info_available ) ;
} else {
this . binding . serverInfoExternalService . setText ( R . string . server_info_unavailable ) ;
}
2018-11-09 16:47:36 +00:00
if ( features . pep ( ) ) {
AxolotlService axolotlService = this . mAccount . getAxolotlService ( ) ;
if ( axolotlService ! = null & & axolotlService . isPepBroken ( ) ) {
this . binding . serverInfoPep . setText ( R . string . server_info_broken ) ;
} else if ( features . pepPublishOptions ( ) | | features . pepOmemoWhitelisted ( ) ) {
this . binding . serverInfoPep . setText ( R . string . server_info_available ) ;
} else {
this . binding . serverInfoPep . setText ( R . string . server_info_partial ) ;
}
} else {
this . binding . serverInfoPep . setText ( R . string . server_info_unavailable ) ;
}
if ( features . httpUpload ( 0 ) ) {
2019-05-27 16:32:04 +00:00
final long maxFileSize = features . getMaxHttpUploadSize ( ) ;
if ( maxFileSize > 0 ) {
this . binding . serverInfoHttpUpload . setText ( UIHelper . filesizeToString ( maxFileSize ) ) ;
} else {
this . binding . serverInfoHttpUpload . setText ( R . string . server_info_available ) ;
}
2018-11-09 16:47:36 +00:00
} else if ( features . p1S3FileTransfer ( ) ) {
this . binding . serverInfoHttpUploadDescription . setText ( R . string . p1_s3_filetransfer ) ;
this . binding . serverInfoHttpUpload . setText ( R . string . server_info_available ) ;
} else {
this . binding . serverInfoHttpUpload . setText ( R . string . server_info_unavailable ) ;
}
this . binding . pushRow . setVisibility ( xmppConnectionService . getPushManagementService ( ) . isStub ( ) ? View . GONE : View . VISIBLE ) ;
if ( xmppConnectionService . getPushManagementService ( ) . available ( mAccount ) ) {
this . binding . serverInfoPush . setText ( R . string . server_info_available ) ;
} else {
this . binding . serverInfoPush . setText ( R . string . server_info_unavailable ) ;
}
final long pgpKeyId = this . mAccount . getPgpId ( ) ;
if ( pgpKeyId ! = 0 & & Config . supportOpenPgp ( ) ) {
OnClickListener openPgp = view - > launchOpenKeyChain ( pgpKeyId ) ;
OnClickListener delete = view - > showDeletePgpDialog ( ) ;
this . binding . pgpFingerprintBox . setVisibility ( View . VISIBLE ) ;
this . binding . pgpFingerprint . setText ( OpenPgpUtils . convertKeyIdToHex ( pgpKeyId ) ) ;
this . binding . pgpFingerprint . setOnClickListener ( openPgp ) ;
if ( " pgp " . equals ( messageFingerprint ) ) {
this . binding . pgpFingerprintDesc . setTextAppearance ( this , R . style . TextAppearance_Conversations_Caption_Highlight ) ;
}
this . binding . pgpFingerprintDesc . setOnClickListener ( openPgp ) ;
this . binding . actionDeletePgp . setOnClickListener ( delete ) ;
} else {
this . binding . pgpFingerprintBox . setVisibility ( View . GONE ) ;
}
final String ownAxolotlFingerprint = this . mAccount . getAxolotlService ( ) . getOwnFingerprint ( ) ;
if ( ownAxolotlFingerprint ! = null & & Config . supportOmemo ( ) ) {
this . binding . axolotlFingerprintBox . setVisibility ( View . VISIBLE ) ;
if ( ownAxolotlFingerprint . equals ( messageFingerprint ) ) {
this . binding . ownFingerprintDesc . setTextAppearance ( this , R . style . TextAppearance_Conversations_Caption_Highlight ) ;
this . binding . ownFingerprintDesc . setText ( R . string . omemo_fingerprint_selected_message ) ;
} else {
this . binding . ownFingerprintDesc . setTextAppearance ( this , R . style . TextAppearance_Conversations_Caption ) ;
this . binding . ownFingerprintDesc . setText ( R . string . omemo_fingerprint ) ;
}
this . binding . axolotlFingerprint . setText ( CryptoHelper . prettifyFingerprint ( ownAxolotlFingerprint . substring ( 2 ) ) ) ;
this . binding . actionCopyAxolotlToClipboard . setVisibility ( View . VISIBLE ) ;
this . binding . actionCopyAxolotlToClipboard . setOnClickListener ( v - > copyOmemoFingerprint ( ownAxolotlFingerprint ) ) ;
} else {
this . binding . axolotlFingerprintBox . setVisibility ( View . GONE ) ;
}
boolean hasKeys = false ;
binding . otherDeviceKeys . removeAllViews ( ) ;
for ( XmppAxolotlSession session : mAccount . getAxolotlService ( ) . findOwnSessions ( ) ) {
if ( ! session . getTrust ( ) . isCompromised ( ) ) {
boolean highlight = session . getFingerprint ( ) . equals ( messageFingerprint ) ;
addFingerprintRow ( binding . otherDeviceKeys , session , highlight ) ;
hasKeys = true ;
}
}
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
this . binding . otherDeviceKeysCard . setVisibility ( View . VISIBLE ) ;
Set < Integer > otherDevices = mAccount . getAxolotlService ( ) . getOwnDeviceIds ( ) ;
if ( otherDevices = = null | | otherDevices . isEmpty ( ) ) {
binding . clearDevices . setVisibility ( View . GONE ) ;
} else {
binding . clearDevices . setVisibility ( View . VISIBLE ) ;
}
} else {
this . binding . otherDeviceKeysCard . setVisibility ( View . GONE ) ;
}
} else {
final TextInputLayout errorLayout ;
if ( this . mAccount . errorStatus ( ) ) {
2018-12-16 22:12:04 +00:00
if ( this . mAccount . getStatus ( ) = = Account . State . UNAUTHORIZED | | this . mAccount . getStatus ( ) = = Account . State . DOWNGRADE_ATTACK ) {
2018-11-09 16:47:36 +00:00
errorLayout = this . binding . accountPasswordLayout ;
} else if ( mShowOptions
& & this . mAccount . getStatus ( ) = = Account . State . SERVER_NOT_FOUND
& & this . binding . hostname . getText ( ) . length ( ) > 0 ) {
errorLayout = this . binding . hostnameLayout ;
} else {
errorLayout = this . binding . accountJidLayout ;
}
errorLayout . setError ( getString ( this . mAccount . getStatus ( ) . getReadableId ( ) ) ) ;
if ( init | | ! accountInfoEdited ( ) ) {
errorLayout . requestFocus ( ) ;
}
} else {
errorLayout = null ;
}
removeErrorsOnAllBut ( errorLayout ) ;
this . binding . stats . setVisibility ( View . GONE ) ;
this . binding . otherDeviceKeysCard . setVisibility ( View . GONE ) ;
}
}
private void updateDisplayName ( String displayName ) {
if ( TextUtils . isEmpty ( displayName ) ) {
this . binding . yourName . setText ( R . string . no_name_set_instructions ) ;
this . binding . yourName . setTextAppearance ( this , R . style . TextAppearance_Conversations_Body1_Tertiary ) ;
} else {
this . binding . yourName . setText ( displayName ) ;
this . binding . yourName . setTextAppearance ( this , R . style . TextAppearance_Conversations_Body1 ) ;
}
}
private void removeErrorsOnAllBut ( TextInputLayout exception ) {
if ( this . binding . accountJidLayout ! = exception ) {
this . binding . accountJidLayout . setErrorEnabled ( false ) ;
this . binding . accountJidLayout . setError ( null ) ;
}
if ( this . binding . accountPasswordLayout ! = exception ) {
this . binding . accountPasswordLayout . setErrorEnabled ( false ) ;
this . binding . accountPasswordLayout . setError ( null ) ;
}
if ( this . binding . hostnameLayout ! = exception ) {
this . binding . hostnameLayout . setErrorEnabled ( false ) ;
this . binding . hostnameLayout . setError ( null ) ;
}
if ( this . binding . portLayout ! = exception ) {
this . binding . portLayout . setErrorEnabled ( false ) ;
this . binding . portLayout . setError ( null ) ;
}
}
private void showDeletePgpDialog ( ) {
AlertDialog . Builder builder = new AlertDialog . Builder ( this ) ;
builder . setTitle ( R . string . unpublish_pgp ) ;
builder . setMessage ( R . string . unpublish_pgp_message ) ;
builder . setNegativeButton ( R . string . cancel , null ) ;
builder . setPositiveButton ( R . string . confirm , ( dialogInterface , i ) - > {
mAccount . setPgpSignId ( 0 ) ;
mAccount . unsetPgpSignature ( ) ;
xmppConnectionService . databaseBackend . updateAccount ( mAccount ) ;
xmppConnectionService . sendPresence ( mAccount ) ;
refreshUiReal ( ) ;
} ) ;
builder . create ( ) . show ( ) ;
}
private void showOsOptimizationWarning ( boolean showBatteryWarning , boolean showDataSaverWarning ) {
this . binding . osOptimization . setVisibility ( showBatteryWarning | | showDataSaverWarning ? View . VISIBLE : View . GONE ) ;
2018-12-10 12:58:05 +00:00
if ( showDataSaverWarning & & android . os . Build . VERSION . SDK_INT > = android . os . Build . VERSION_CODES . N ) {
2018-11-09 16:47:36 +00:00
this . binding . osOptimizationHeadline . setText ( R . string . data_saver_enabled ) ;
this . binding . osOptimizationBody . setText ( R . string . data_saver_enabled_explained ) ;
this . binding . osOptimizationDisable . setText ( R . string . allow ) ;
this . binding . osOptimizationDisable . setOnClickListener ( v - > {
Intent intent = new Intent ( Settings . ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS ) ;
Uri uri = Uri . parse ( " package: " + getPackageName ( ) ) ;
intent . setData ( uri ) ;
try {
startActivityForResult ( intent , REQUEST_DATA_SAVER ) ;
} catch ( ActivityNotFoundException e ) {
Toast . makeText ( EditAccountActivity . this , R . string . device_does_not_support_data_saver , Toast . LENGTH_SHORT ) . show ( ) ;
}
} ) ;
2018-12-10 12:58:05 +00:00
} else if ( showBatteryWarning & & android . os . Build . VERSION . SDK_INT > = android . os . Build . VERSION_CODES . M ) {
2018-11-09 16:47:36 +00:00
this . binding . osOptimizationDisable . setText ( R . string . disable ) ;
this . binding . osOptimizationHeadline . setText ( R . string . battery_optimizations_enabled ) ;
this . binding . osOptimizationBody . setText ( R . string . battery_optimizations_enabled_explained ) ;
this . binding . osOptimizationDisable . setOnClickListener ( v - > {
Intent intent = new Intent ( Settings . ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS ) ;
Uri uri = Uri . parse ( " package: " + getPackageName ( ) ) ;
intent . setData ( uri ) ;
try {
startActivityForResult ( intent , REQUEST_BATTERY_OP ) ;
} catch ( ActivityNotFoundException e ) {
Toast . makeText ( EditAccountActivity . this , R . string . device_does_not_support_battery_op , Toast . LENGTH_SHORT ) . show ( ) ;
}
} ) ;
}
}
public void showWipePepDialog ( ) {
Builder builder = new Builder ( this ) ;
builder . setTitle ( getString ( R . string . clear_other_devices ) ) ;
builder . setIconAttribute ( android . R . attr . alertDialogIcon ) ;
builder . setMessage ( getString ( R . string . clear_other_devices_desc ) ) ;
builder . setNegativeButton ( getString ( R . string . cancel ) , null ) ;
builder . setPositiveButton ( getString ( R . string . accept ) ,
( dialog , which ) - > mAccount . getAxolotlService ( ) . wipeOtherPepDevices ( ) ) ;
builder . create ( ) . show ( ) ;
}
private void editMamPrefs ( ) {
this . mFetchingMamPrefsToast = Toast . makeText ( this , R . string . fetching_mam_prefs , Toast . LENGTH_LONG ) ;
this . mFetchingMamPrefsToast . show ( ) ;
xmppConnectionService . fetchMamPreferences ( mAccount , this ) ;
}
@Override
public void onKeyStatusUpdated ( AxolotlService . FetchStatus report ) {
refreshUi ( ) ;
}
@Override
public void onCaptchaRequested ( final Account account , final String id , final Data data , final Bitmap captcha ) {
runOnUiThread ( ( ) - > {
if ( mCaptchaDialog ! = null & & mCaptchaDialog . isShowing ( ) ) {
mCaptchaDialog . dismiss ( ) ;
}
final Builder builder = new Builder ( EditAccountActivity . this ) ;
final View view = getLayoutInflater ( ) . inflate ( R . layout . captcha , null ) ;
final ImageView imageView = view . findViewById ( R . id . captcha ) ;
final EditText input = view . findViewById ( R . id . input ) ;
imageView . setImageBitmap ( captcha ) ;
builder . setTitle ( getString ( R . string . captcha_required ) ) ;
builder . setView ( view ) ;
builder . setPositiveButton ( getString ( R . string . ok ) ,
( dialog , which ) - > {
String rc = input . getText ( ) . toString ( ) ;
data . put ( " username " , account . getUsername ( ) ) ;
data . put ( " password " , account . getPassword ( ) ) ;
data . put ( " ocr " , rc ) ;
data . submit ( ) ;
if ( xmppConnectionServiceBound ) {
xmppConnectionService . sendCreateAccountWithCaptchaPacket ( account , id , data ) ;
}
} ) ;
builder . setNegativeButton ( getString ( R . string . cancel ) , ( dialog , which ) - > {
if ( xmppConnectionService ! = null ) {
xmppConnectionService . sendCreateAccountWithCaptchaPacket ( account , null , null ) ;
}
} ) ;
builder . setOnCancelListener ( dialog - > {
if ( xmppConnectionService ! = null ) {
xmppConnectionService . sendCreateAccountWithCaptchaPacket ( account , null , null ) ;
}
} ) ;
mCaptchaDialog = builder . create ( ) ;
mCaptchaDialog . show ( ) ;
input . requestFocus ( ) ;
} ) ;
}
public void onShowErrorToast ( final int resId ) {
runOnUiThread ( ( ) - > Toast . makeText ( EditAccountActivity . this , resId , Toast . LENGTH_SHORT ) . show ( ) ) ;
}
@Override
public void onPreferencesFetched ( final Element prefs ) {
runOnUiThread ( ( ) - > {
if ( mFetchingMamPrefsToast ! = null ) {
mFetchingMamPrefsToast . cancel ( ) ;
}
Builder builder = new Builder ( EditAccountActivity . this ) ;
builder . setTitle ( R . string . server_side_mam_prefs ) ;
String defaultAttr = prefs . getAttribute ( " default " ) ;
final List < String > defaults = Arrays . asList ( " never " , " roster " , " always " ) ;
final AtomicInteger choice = new AtomicInteger ( Math . max ( 0 , defaults . indexOf ( defaultAttr ) ) ) ;
builder . setSingleChoiceItems ( R . array . mam_prefs , choice . get ( ) , ( dialog , which ) - > choice . set ( which ) ) ;
builder . setNegativeButton ( R . string . cancel , null ) ;
builder . setPositiveButton ( R . string . ok , ( dialog , which ) - > {
prefs . setAttribute ( " default " , defaults . get ( choice . get ( ) ) ) ;
xmppConnectionService . pushMamPreferences ( mAccount , prefs ) ;
} ) ;
builder . create ( ) . show ( ) ;
} ) ;
}
@Override
public void onPreferencesFetchFailed ( ) {
runOnUiThread ( ( ) - > {
if ( mFetchingMamPrefsToast ! = null ) {
mFetchingMamPrefsToast . cancel ( ) ;
}
Toast . makeText ( EditAccountActivity . this , R . string . unable_to_fetch_mam_prefs , Toast . LENGTH_LONG ) . show ( ) ;
} ) ;
}
@Override
public void OnUpdateBlocklist ( Status status ) {
refreshUi ( ) ;
}
2014-08-15 15:31:24 +00:00
}