store connection settings after pressing submit in hostname fragment
This commit is contained in:
parent
99c11fba17
commit
d54978f593
|
@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData;
|
|||
import androidx.room.Dao;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Transaction;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import im.conversations.android.database.entity.AccountEntity;
|
||||
import im.conversations.android.database.model.Account;
|
||||
|
@ -13,64 +14,80 @@ import java.util.List;
|
|||
import org.jxmpp.jid.BareJid;
|
||||
|
||||
@Dao
|
||||
public interface AccountDao {
|
||||
public abstract class AccountDao {
|
||||
|
||||
@Query("SELECT EXISTS (SELECT id FROM account WHERE address=:address)")
|
||||
boolean hasAccount(BareJid address);
|
||||
public abstract boolean hasAccount(BareJid address);
|
||||
|
||||
@Query("SELECT NOT EXISTS (SELECT id FROM account)")
|
||||
LiveData<Boolean> hasNoAccounts();
|
||||
public abstract LiveData<Boolean> hasNoAccounts();
|
||||
|
||||
@Insert
|
||||
long insert(final AccountEntity account);
|
||||
public abstract long insert(final AccountEntity account);
|
||||
|
||||
@Query("SELECT id,address,randomSeed FROM account WHERE enabled = 1")
|
||||
ListenableFuture<List<Account>> getEnabledAccounts();
|
||||
public abstract ListenableFuture<List<Account>> getEnabledAccounts();
|
||||
|
||||
@Query("SELECT id,address,randomSeed FROM account WHERE address=:address AND enabled=1")
|
||||
ListenableFuture<Account> getEnabledAccount(BareJid address);
|
||||
public abstract ListenableFuture<Account> getEnabledAccount(BareJid address);
|
||||
|
||||
@Query("SELECT id,address,randomSeed FROM account WHERE id=:id AND enabled=1")
|
||||
ListenableFuture<Account> getEnabledAccount(long id);
|
||||
public abstract ListenableFuture<Account> getEnabledAccount(long id);
|
||||
|
||||
@Query("SELECT id,address FROM account")
|
||||
LiveData<List<AccountIdentifier>> getAccounts();
|
||||
public abstract LiveData<List<AccountIdentifier>> getAccounts();
|
||||
|
||||
@Query("SELECT hostname,port,directTls FROM account WHERE id=:id AND hostname != null")
|
||||
Connection getConnectionSettings(long id);
|
||||
@Query("SELECT hostname,port,directTls FROM account WHERE id=:id AND hostname IS NOT null")
|
||||
public abstract Connection getConnectionSettings(long id);
|
||||
|
||||
@Query("SELECT resource FROM account WHERE id=:id")
|
||||
String getResource(long id);
|
||||
public abstract String getResource(long id);
|
||||
|
||||
@Query("SELECT rosterVersion FROM account WHERE id=:id")
|
||||
String getRosterVersion(long id);
|
||||
public abstract String getRosterVersion(long id);
|
||||
|
||||
@Query("SELECT quickStartAvailable FROM account where id=:id")
|
||||
boolean quickStartAvailable(long id);
|
||||
public abstract boolean quickStartAvailable(long id);
|
||||
|
||||
@Query("SELECT loginAndBind FROM account where id=:id")
|
||||
boolean loginAndBind(long id);
|
||||
public abstract boolean loginAndBind(long id);
|
||||
|
||||
@Query(
|
||||
"UPDATE account set quickStartAvailable=:available WHERE id=:id AND"
|
||||
+ " quickStartAvailable != :available")
|
||||
void setQuickStartAvailable(long id, boolean available);
|
||||
public abstract void setQuickStartAvailable(long id, boolean available);
|
||||
|
||||
@Query(
|
||||
"UPDATE account set loginAndBind=:loginAndBind WHERE id=:id AND"
|
||||
+ " loginAndBind != :loginAndBind")
|
||||
void setLoginAndBind(long id, boolean loginAndBind);
|
||||
public abstract void setLoginAndBind(long id, boolean loginAndBind);
|
||||
|
||||
@Query(
|
||||
"UPDATE account set showErrorNotification=:showErrorNotification WHERE id=:id AND"
|
||||
+ " showErrorNotification != :showErrorNotification")
|
||||
int setShowErrorNotification(long id, boolean showErrorNotification);
|
||||
public abstract int setShowErrorNotification(long id, boolean showErrorNotification);
|
||||
|
||||
@Query("UPDATE account set resource=:resource WHERE id=:id")
|
||||
void setResource(long id, String resource);
|
||||
public abstract void setResource(long id, String resource);
|
||||
|
||||
@Query("DELETE FROM account WHERE id=:id")
|
||||
int delete(long id);
|
||||
public abstract int delete(long id);
|
||||
|
||||
@Query(
|
||||
"UPDATE account SET hostname=:hostname, port=:port, directTls=:directTls WHERE"
|
||||
+ " id=:account")
|
||||
protected abstract int setConnection(
|
||||
long account, String hostname, int port, boolean directTls);
|
||||
|
||||
@Transaction
|
||||
public void setConnection(final Account account, final Connection connection) {
|
||||
final var count =
|
||||
setConnection(
|
||||
account.id, connection.hostname, connection.port, connection.directTls);
|
||||
if (count != 1) {
|
||||
throw new IllegalStateException("Could not update account");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO on disable set resource to null
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package im.conversations.android.database.model;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
|
||||
public class Connection {
|
||||
|
||||
public final String hostname;
|
||||
|
@ -11,4 +13,13 @@ public class Connection {
|
|||
this.port = port;
|
||||
this.directTls = directTls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("hostname", hostname)
|
||||
.add("port", port)
|
||||
.add("directTls", directTls)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import im.conversations.android.database.CredentialStore;
|
|||
import im.conversations.android.database.entity.AccountEntity;
|
||||
import im.conversations.android.database.model.Account;
|
||||
import im.conversations.android.database.model.AccountIdentifier;
|
||||
import im.conversations.android.database.model.Connection;
|
||||
import im.conversations.android.xmpp.ConnectionPool;
|
||||
import im.conversations.android.xmpp.XmppConnection;
|
||||
import im.conversations.android.xmpp.manager.RegistrationManager;
|
||||
|
@ -74,7 +75,7 @@ public class AccountRepository extends AbstractRepository {
|
|||
}
|
||||
|
||||
public ListenableFuture<Void> deleteAccountAsync(@NonNull Account account) {
|
||||
return Futures.submit(() -> deleteAccount(account), IO_EXECUTOR);
|
||||
return Futures.submit(() -> deleteAccount(account), database.getQueryExecutor());
|
||||
}
|
||||
|
||||
private Void deleteAccount(@NonNull Account account) {
|
||||
|
@ -86,7 +87,7 @@ public class AccountRepository extends AbstractRepository {
|
|||
public ListenableFuture<XmppConnection> getConnectedFuture(@NonNull final Account account) {
|
||||
final var optional = ConnectionPool.getInstance(context).get(account);
|
||||
if (optional.isPresent()) {
|
||||
return optional.get().asConnectedFuture();
|
||||
return optional.get().asConnectedFuture(false);
|
||||
} else {
|
||||
return Futures.immediateFailedFuture(
|
||||
new IllegalStateException(
|
||||
|
@ -106,6 +107,18 @@ public class AccountRepository extends AbstractRepository {
|
|||
return account;
|
||||
}
|
||||
|
||||
public ListenableFuture<Account> setConnectionAsync(
|
||||
final Account account, final Connection connection) {
|
||||
return Futures.submit(
|
||||
() -> setConnection(account, connection), database.getQueryExecutor());
|
||||
}
|
||||
|
||||
public Account setConnection(final Account account, final Connection connection) {
|
||||
database.accountDao().setConnection(account, connection);
|
||||
ConnectionPool.getInstance(context).reconnect(account);
|
||||
return account;
|
||||
}
|
||||
|
||||
public void reconnect(final Account account) {
|
||||
ConnectionPool.getInstance(context).reconnect(account);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.os.Bundle;
|
|||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.NavController;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import im.conversations.android.R;
|
||||
import im.conversations.android.SetupNavigationDirections;
|
||||
import im.conversations.android.databinding.ActivitySetupBinding;
|
||||
|
@ -30,9 +31,19 @@ public class SetupActivity extends BaseActivity {
|
|||
new ViewModelProvider(this, getDefaultViewModelProviderFactory());
|
||||
this.setupViewModel = viewModelProvider.get(SetupViewModel.class);
|
||||
this.setupViewModel.getRedirection().observe(this, this::onRedirectionEvent);
|
||||
this.setupViewModel.getGenericErrorEvent().observe(this, this::onGenericErrorEvent);
|
||||
Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
|
||||
}
|
||||
|
||||
private void onGenericErrorEvent(final Event<String> errorEvent) {
|
||||
if (errorEvent.isConsumable()) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setMessage(errorEvent.consume())
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void onRedirectionEvent(final Event<SetupViewModel.Target> targetEvent) {
|
||||
if (targetEvent.isConsumable()) {
|
||||
final NavController navController = getNavController();
|
||||
|
|
|
@ -6,19 +6,24 @@ import androidx.lifecycle.AndroidViewModel;
|
|||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import im.conversations.android.R;
|
||||
import im.conversations.android.database.model.Account;
|
||||
import im.conversations.android.database.model.Connection;
|
||||
import im.conversations.android.repository.AccountRepository;
|
||||
import im.conversations.android.ui.Event;
|
||||
import im.conversations.android.util.ConnectionStates;
|
||||
import im.conversations.android.xmpp.ConnectionException;
|
||||
import im.conversations.android.xmpp.ConnectionState;
|
||||
import im.conversations.android.xmpp.XmppConnection;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jxmpp.jid.BareJid;
|
||||
import org.jxmpp.jid.impl.JidCreate;
|
||||
|
@ -35,8 +40,12 @@ public class SetupViewModel extends AndroidViewModel {
|
|||
private final MutableLiveData<String> password = new MutableLiveData<>();
|
||||
private final MutableLiveData<String> passwordError = new MutableLiveData<>();
|
||||
private final MutableLiveData<String> hostname = new MutableLiveData<>();
|
||||
private final MutableLiveData<String> hostnameError = new MutableLiveData<>();
|
||||
private final MutableLiveData<String> port = new MutableLiveData<>();
|
||||
private final MutableLiveData<String> portError = new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> opportunisticTls = new MutableLiveData<>();
|
||||
|
||||
private final MutableLiveData<Event<String>> genericErrorEvent = new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> loading = new MutableLiveData<>(false);
|
||||
|
||||
private final MutableLiveData<Event<Target>> redirection = new MutableLiveData<>();
|
||||
|
@ -54,6 +63,9 @@ public class SetupViewModel extends AndroidViewModel {
|
|||
.observeForever(s -> xmppAddressError.postValue(null));
|
||||
Transformations.distinctUntilChanged(password)
|
||||
.observeForever(s -> passwordError.postValue(null));
|
||||
Transformations.distinctUntilChanged(port).observeForever(s -> portError.postValue(null));
|
||||
Transformations.distinctUntilChanged(hostname)
|
||||
.observeForever(s -> hostnameError.postValue(null));
|
||||
}
|
||||
|
||||
public LiveData<Boolean> isLoading() {
|
||||
|
@ -76,10 +88,18 @@ public class SetupViewModel extends AndroidViewModel {
|
|||
return hostname;
|
||||
}
|
||||
|
||||
public LiveData<String> getHostnameError() {
|
||||
return this.hostnameError;
|
||||
}
|
||||
|
||||
public MutableLiveData<String> getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public LiveData<String> getPortError() {
|
||||
return this.portError;
|
||||
}
|
||||
|
||||
public MutableLiveData<Boolean> getOpportunisticTls() {
|
||||
return this.opportunisticTls;
|
||||
}
|
||||
|
@ -88,6 +108,10 @@ public class SetupViewModel extends AndroidViewModel {
|
|||
return Transformations.distinctUntilChanged(this.passwordError);
|
||||
}
|
||||
|
||||
public LiveData<Event<String>> getGenericErrorEvent() {
|
||||
return this.genericErrorEvent;
|
||||
}
|
||||
|
||||
public boolean submitXmppAddress() {
|
||||
final var account = this.account;
|
||||
final var userInput = Strings.nullToEmpty(this.xmppAddress.getValue()).trim();
|
||||
|
@ -111,6 +135,14 @@ public class SetupViewModel extends AndroidViewModel {
|
|||
return true;
|
||||
} else {
|
||||
this.account = null;
|
||||
|
||||
// when the XMPP address changes we want to reset connection info too
|
||||
// this is partially to indicate that Conversations might not actually use those
|
||||
// connection settings if the connection works without them
|
||||
this.hostname.setValue(null);
|
||||
this.port.setValue(null);
|
||||
this.opportunisticTls.setValue(false);
|
||||
|
||||
this.accountRepository.deleteAccountAsync(account);
|
||||
}
|
||||
}
|
||||
|
@ -210,7 +242,8 @@ public class SetupViewModel extends AndroidViewModel {
|
|||
return;
|
||||
}
|
||||
}
|
||||
// TODO show generic error
|
||||
this.genericErrorEvent.postValue(
|
||||
new Event<>(getApplication().getString(ConnectionStates.toStringRes(state))));
|
||||
}
|
||||
|
||||
private boolean redirectIfNecessary(final Target current, final Target next) {
|
||||
|
@ -231,7 +264,37 @@ public class SetupViewModel extends AndroidViewModel {
|
|||
this.redirectIfNecessary(Target.ENTER_HOSTNAME, Target.ENTER_ADDRESS);
|
||||
return true;
|
||||
}
|
||||
final String hostname =
|
||||
Strings.nullToEmpty(this.hostname.getValue()).trim().toLowerCase(Locale.ROOT);
|
||||
if (hostname.isEmpty() || CharMatcher.whitespace().matchesAnyOf(hostname)) {
|
||||
this.hostnameError.postValue(getApplication().getString(R.string.not_valid_hostname));
|
||||
return true;
|
||||
}
|
||||
final Integer port = Ints.tryParse(Strings.nullToEmpty(this.port.getValue()));
|
||||
if (port == null || port < 0 || port > 65535) {
|
||||
this.portError.postValue(getApplication().getString(R.string.invalid));
|
||||
return true;
|
||||
}
|
||||
final boolean directTls = Boolean.FALSE.equals(this.opportunisticTls.getValue());
|
||||
final var connection = new Connection(hostname, port, directTls);
|
||||
final var setConnectionFuture =
|
||||
this.accountRepository.setConnectionAsync(account, connection);
|
||||
this.setCurrentOperation(setConnectionFuture);
|
||||
Futures.addCallback(
|
||||
setConnectionFuture,
|
||||
new FutureCallback<>() {
|
||||
@Override
|
||||
public void onSuccess(final Account result) {
|
||||
decideNextStep(Target.ENTER_HOSTNAME, account);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull final Throwable throwable) {
|
||||
loading.postValue(false);
|
||||
// TODO error message?!
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package im.conversations.android.util;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import im.conversations.android.R;
|
||||
import im.conversations.android.xmpp.ConnectionState;
|
||||
|
||||
public final class ConnectionStates {
|
||||
|
||||
private ConnectionStates() {
|
||||
throw new IllegalStateException("Do not instantiate me");
|
||||
}
|
||||
|
||||
@StringRes
|
||||
public static int toStringRes(final ConnectionState state) {
|
||||
switch (state) {
|
||||
case ONLINE:
|
||||
return R.string.account_status_online;
|
||||
case CONNECTING:
|
||||
return R.string.account_status_connecting;
|
||||
case OFFLINE:
|
||||
return R.string.account_status_offline;
|
||||
case UNAUTHORIZED:
|
||||
return R.string.account_status_unauthorized;
|
||||
case SERVER_NOT_FOUND:
|
||||
return R.string.account_status_not_found;
|
||||
case TLS_ERROR:
|
||||
return R.string.account_status_tls_error;
|
||||
case TLS_ERROR_DOMAIN:
|
||||
return R.string.account_status_tls_error_domain;
|
||||
case INCOMPATIBLE_SERVER:
|
||||
return R.string.account_status_incompatible_server;
|
||||
case INCOMPATIBLE_CLIENT:
|
||||
return R.string.account_status_incompatible_client;
|
||||
case TOR_NOT_AVAILABLE:
|
||||
return R.string.account_status_tor_unavailable;
|
||||
case BIND_FAILURE:
|
||||
return R.string.account_status_bind_failure;
|
||||
case SESSION_FAILURE:
|
||||
return R.string.session_failure;
|
||||
case DOWNGRADE_ATTACK:
|
||||
return R.string.sasl_downgrade;
|
||||
case HOST_UNKNOWN:
|
||||
return R.string.account_status_host_unknown;
|
||||
case POLICY_VIOLATION:
|
||||
return R.string.account_status_policy_violation;
|
||||
case REGISTRATION_PLEASE_WAIT:
|
||||
return R.string.registration_please_wait;
|
||||
case REGISTRATION_PASSWORD_TOO_WEAK:
|
||||
return R.string.registration_password_too_weak;
|
||||
case STREAM_ERROR:
|
||||
return R.string.account_status_stream_error;
|
||||
case STREAM_OPENING_ERROR:
|
||||
return R.string.account_status_stream_opening_error;
|
||||
case PAYMENT_REQUIRED:
|
||||
return R.string.payment_required;
|
||||
case MISSING_INTERNET_PERMISSION:
|
||||
return R.string.missing_internet_permission;
|
||||
case TEMPORARY_AUTH_FAILURE:
|
||||
return R.string.account_status_temporary_auth_failure;
|
||||
default:
|
||||
throw new IllegalStateException(String.format("no string res for %s", state));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -78,6 +78,7 @@ import java.security.Principal;
|
|||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Iterator;
|
||||
|
@ -97,7 +98,6 @@ import javax.net.ssl.SSLSocket;
|
|||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.X509KeyManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import okhttp3.HttpUrl;
|
||||
import org.jxmpp.jid.Jid;
|
||||
import org.jxmpp.jid.impl.JidCreate;
|
||||
import org.jxmpp.stringprep.XmppStringprepException;
|
||||
|
@ -151,7 +151,6 @@ public class XmppConnection implements Runnable {
|
|||
private final PendingItem<SettableFuture<XmppConnection>> connectedFuture = new PendingItem<>();
|
||||
private SaslMechanism saslMechanism;
|
||||
private HashedToken.Mechanism hashTokenRequest;
|
||||
private HttpUrl redirectionUrl = null;
|
||||
private String verifiedHostname = null;
|
||||
private volatile Thread mThread;
|
||||
private CountDownLatch mStreamCountDownLatch;
|
||||
|
@ -1392,26 +1391,12 @@ public class XmppConnection implements Runnable {
|
|||
return bind;
|
||||
}
|
||||
|
||||
private void setAccountCreationFailed(final String url) {
|
||||
final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url);
|
||||
if (httpUrl != null && httpUrl.isHttps()) {
|
||||
this.redirectionUrl = httpUrl;
|
||||
throw new StateChangingError(ConnectionState.REGISTRATION_WEB);
|
||||
}
|
||||
throw new StateChangingError(ConnectionState.REGISTRATION_FAILED);
|
||||
}
|
||||
|
||||
public HttpUrl getRedirectionUrl() {
|
||||
return this.redirectionUrl;
|
||||
}
|
||||
|
||||
public void resetEverything() {
|
||||
resetAttemptCount(true);
|
||||
resetStreamId();
|
||||
clearIqCallbacks();
|
||||
this.stanzasSent = 0;
|
||||
mStanzaQueue.clear();
|
||||
this.redirectionUrl = null;
|
||||
this.saslMechanism = null;
|
||||
}
|
||||
|
||||
|
@ -1892,13 +1877,19 @@ public class XmppConnection implements Runnable {
|
|||
this.statusListener = listener;
|
||||
}
|
||||
|
||||
public ListenableFuture<XmppConnection> asConnectedFuture() {
|
||||
public ListenableFuture<XmppConnection> asConnectedFuture(final boolean waitOnError) {
|
||||
synchronized (this) {
|
||||
final var state = this.connectionState;
|
||||
// TODO some more permanent errors like 'unauthorized' should also return immediate
|
||||
if (this.connectionState == ConnectionState.ONLINE) {
|
||||
return Futures.immediateFuture(this);
|
||||
}
|
||||
} else if (Arrays.asList(ConnectionState.OFFLINE, ConnectionState.CONNECTING)
|
||||
.contains(state)
|
||||
|| waitOnError) {
|
||||
return this.connectedFuture.peekOrCreate(SettableFuture::create);
|
||||
} else {
|
||||
return Futures.immediateFailedFuture(new ConnectionException(state));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -102,10 +102,10 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:layout_weight="3"
|
||||
android:layout_weight="5"
|
||||
android:enabled="@{!setupViewModel.isLoading()}"
|
||||
android:hint="@string/account_settings_hostname"
|
||||
app:errorText="@{setupViewModel.xmppAddressError}">
|
||||
app:errorText="@{setupViewModel.hostnameError}">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/hostname"
|
||||
|
@ -124,10 +124,10 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_weight="2"
|
||||
android:enabled="@{!setupViewModel.isLoading()}"
|
||||
android:hint="@string/account_settings_port"
|
||||
app:errorText="@{setupViewModel.xmppAddressError}">
|
||||
app:errorText="@{setupViewModel.portError}">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/port"
|
||||
|
@ -136,6 +136,7 @@
|
|||
android:imeOptions="flagNoExtractUi|actionNext"
|
||||
android:inputType="number"
|
||||
android:maxLines="1"
|
||||
android:maxLength="5"
|
||||
android:text="@={setupViewModel.port}"
|
||||
app:editorAction="@{()->setupViewModel.submitHostname()}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
|
|
@ -1040,5 +1040,6 @@
|
|||
<string name="pref_category_receiving">Receiving</string>
|
||||
<string name="use_opportunistic_tls">Opportunistic TLS (STARTTLS)</string>
|
||||
<string name="info_required">Info required</string>
|
||||
<string name="invalid">Invalid!</string>
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue