Too lazy to comment. Simplifications and rewrites of login

This commit is contained in:
Paul Schaub 2019-12-21 00:27:48 +01:00
parent ccddad2e31
commit 3569462a78
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
43 changed files with 451 additions and 708 deletions

View File

@ -4,7 +4,6 @@ import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.data.di.RepositoryModule;
import org.mercury_im.messenger.di.module.AndroidPersistenceModule;
import org.mercury_im.messenger.di.module.AppModule;
import org.mercury_im.messenger.di.module.MercuryModule;
import org.mercury_im.messenger.service.MercuryConnectionService;
import org.mercury_im.messenger.ui.MainActivity;
import org.mercury_im.messenger.ui.chat.ChatActivity;
@ -30,8 +29,7 @@ import dagger.Component;
modules = {
AppModule.class,
AndroidPersistenceModule.class,
RepositoryModule.class,
MercuryModule.class
RepositoryModule.class
})
public interface AppComponent {

View File

@ -1,7 +1,6 @@
package org.mercury_im.messenger.ui.login;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
@ -14,7 +13,6 @@ import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.textfield.TextInputEditText;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.account.error.PasswordError;
import org.mercury_im.messenger.account.error.UsernameError;
@ -54,42 +52,42 @@ public class LoginActivity extends AppCompatActivity implements TextView.OnEdito
viewModel = new ViewModelProvider(this).get(LoginViewModel.class);
observeViewModel();
setupTextInputFields();
mSignInView.setOnClickListener(view -> viewModel.loginDetailsEntered());
mSignInView.setOnClickListener(view -> viewModel.login());
}
private void observeViewModel() {
observeUsernameError();
observePasswordError();
finishOnceLoginWasSuccessful();
displayCredentials(viewModel.getAccount());
enableLoginButtonOncePossible();
}
private void observeUsernameError() {
viewModel.getUsernameError().observe(this, usernameError -> {
Optional<String> errorMessage = getUsernameError(usernameError);
if (errorMessage.isPresent()) {
mUsernameView.setError(errorMessage.getItem());
}
mUsernameView.setError(usernameError.isError() ? usernameError.getErrorMessage() : null);
});
}
private void observePasswordError() {
viewModel.getPasswordError().observe(this, passwordError -> {
Optional<String> errorMessage = getPasswordError(passwordError);
if (errorMessage.isPresent()) {
mPasswordView.setError(errorMessage.getItem());
}
mPasswordView.setError(passwordError.isError() ? passwordError.getErrorMessage() : null);
});
}
private void finishOnceLoginWasSuccessful() {
viewModel.getSigninSuccessful().observe(this, successful -> {
viewModel.isLoginSuccessful().observe(this, successful -> {
if (Boolean.TRUE.equals(successful)) {
finish();
}
});
}
private void enableLoginButtonOncePossible() {
viewModel.isLoginEnabled().observe(this, isEnabled -> {
mSignInView.setEnabled(isEnabled);
});
}
private Optional<String> getUsernameError(UsernameError usernameError) {
switch (usernameError) {
case none:
@ -130,8 +128,8 @@ public class LoginActivity extends AppCompatActivity implements TextView.OnEdito
mUsernameView.setText(accountEvent.getAddress());
}
if (accountEvent.getAuthentication() != null) {
mPasswordView.setText(accountEvent.getAuthentication().getPassword());
if (accountEvent.getPassword() != null) {
mPasswordView.setText(accountEvent.getPassword());
}
});
}
@ -143,16 +141,14 @@ public class LoginActivity extends AppCompatActivity implements TextView.OnEdito
mUsernameView.addTextChangedListener(new TextChangedListener() {
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
viewModel.onUsernameInputChanged(charSequence.toString());
Log.d(Messenger.TAG, "onTextChanged");
viewModel.setUsername(charSequence.toString());
}
});
mPasswordView.addTextChangedListener(new TextChangedListener() {
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
viewModel.onPasswordInputChanged(charSequence.toString());
Log.d(Messenger.TAG, "onTextChanged");
viewModel.setPassword(charSequence.toString());
}
});
}
@ -169,7 +165,7 @@ public class LoginActivity extends AppCompatActivity implements TextView.OnEdito
case R.id.password:
if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) {
viewModel.loginDetailsEntered();
viewModel.login();
return true;
}
}

View File

@ -1,140 +1,138 @@
package org.mercury_im.messenger.ui.login;
import android.text.TextUtils;
import android.util.Log;
import android.app.Application;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import io.reactivex.Single;
import io.reactivex.observers.DisposableSingleObserver;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.account.error.PasswordError;
import org.mercury_im.messenger.account.error.UsernameError;
import org.mercury_im.messenger.data.repository.AccountRepository;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.entity.IAccount;
import org.mercury_im.messenger.entity.PasswordAuthentication;
import javax.inject.Inject;
public class LoginViewModel extends ViewModel {
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
@Inject
AccountRepository accountRepository;
public class LoginViewModel extends AndroidViewModel {
// @Inject
// ConnectionCenter connectionCenter;
private MutableLiveData<Error> usernameError = new MutableLiveData<>(new Error());
private MutableLiveData<Error> passwordError = new MutableLiveData<>(new Error());
private MutableLiveData<Boolean> loginEnabled = new MutableLiveData<>(false);
private MutableLiveData<Boolean> loginSuccessful = new MutableLiveData<>(false);
private String username;
private EntityBareJid username;
private String password;
private MutableLiveData<UsernameError> usernameError = new MutableLiveData<>(UsernameError.none);
private MutableLiveData<PasswordError> passwordError = new MutableLiveData<>(PasswordError.none);
@Inject
Messenger messenger;
private MutableLiveData<Account> account = new MutableLiveData<>();
private MutableLiveData<Boolean> signinSuccessful = new MutableLiveData<>();
public LoginViewModel() {
super();
MercuryImApplication.getApplication().getAppComponent().inject(this);
init(new IAccount());
public LoginViewModel(@NonNull Application application) {
super(application);
((MercuryImApplication) application).getAppComponent().inject(this);
}
public LiveData<Boolean> getSigninSuccessful() {
return signinSuccessful;
public void setUsername(String username) {
if (username == null || username.isEmpty()) {
usernameError.setValue(new Error(getApplication().getResources().getString(R.string.error_field_required)));
this.username = null;
updateLoginEnabled();
return;
}
try {
this.username = JidCreate.entityBareFrom(username);
updateLoginEnabled();
} catch (XmppStringprepException e) {
usernameError.setValue(new Error(getApplication().getResources().getString(R.string.error_invalid_username)));
this.username = null;
updateLoginEnabled();
}
}
public void onUsernameInputChanged(String input) {
this.username = input;
public void setPassword(String password) {
this.password = password;
updateLoginEnabled();
if (password == null || password.isEmpty()) {
passwordError.setValue(new Error(getApplication().getResources().getString(R.string.error_field_required)));
}
}
public void onPasswordInputChanged(String input) {
this.password = input;
}
public LiveData<UsernameError> getUsernameError() {
return usernameError;
}
public LiveData<PasswordError> getPasswordError() {
return passwordError;
}
/**
* Try to parse the input string into a {@link EntityBareJid} and return it.
* Return null on failure.
* @param input input string
* @return valid username or null
*/
private EntityBareJid asValidUsernameOrNull(String input) {
return JidCreate.entityBareFromOrNull(input);
}
private boolean isPasswordValid(String password) {
return !password.isEmpty();
}
public void init(@NonNull Account account) {
this.account.setValue(account);
}
public MutableLiveData<Account> getAccount() {
return account;
private void updateLoginEnabled() {
loginEnabled.setValue(username != null && !(password == null || password.isEmpty()));
}
public void login() {
Account account = getAccount().getValue();
if (account != null && account.getAddress() != null
&& !TextUtils.isEmpty(account.getAuthentication().getPassword())) {
accountRepository.upsertAccount(account);
}
Account account = new IAccount();
account.setAddress(username.asUnescapedString());
account.setPassword(password);
account.setEnabled(true);
loginEnabled.setValue(false);
messenger.addAccount().enableAccountAndLogin(account)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.map(result -> {
switch (result) {
case success:
loginSuccessful.setValue(true);
break;
case credential_error:
passwordError.setValue(new Error(getApplication().getResources().getString(R.string.error_incorrect_password)));
loginEnabled.setValue(true);
break;
case server_error:
case other_error:
Toast.makeText(getApplication(), "A connection error occurred", Toast.LENGTH_LONG);
loginEnabled.setValue(true);
}
return true;
})
.ignoreElement()
.subscribe();
}
public void loginDetailsEntered() {
boolean loginIntact = true;
if (username.isEmpty()) {
usernameError.postValue(UsernameError.emptyUsername);
loginIntact = false;
public LiveData<Error> getPasswordError() {
return passwordError;
}
public LiveData<Error> getUsernameError() {
return usernameError;
}
public LiveData<Boolean> isLoginSuccessful() {
return loginSuccessful;
}
public LiveData<Boolean> isLoginEnabled() {
return loginEnabled;
}
public static class Error {
private String message;
public Error() {
}
EntityBareJid bareJid = asValidUsernameOrNull(username);
if (bareJid == null) {
usernameError.postValue(UsernameError.invalidUsername);
loginIntact = false;
public Error(String errorMessage) {
this.message = errorMessage;
}
if (!isPasswordValid(password)) {
passwordError.postValue(PasswordError.invalidPassword);
loginIntact = false;
public boolean isError() {
return message != null;
}
if (loginIntact) {
Account account = new IAccount();
account.setEnabled(true);
account.setAddress(bareJid.toString());
account.setAuthentication(new PasswordAuthentication(password));
Single<Account> insert = accountRepository.upsertAccount(account);
insert.subscribe(
new DisposableSingleObserver<Account>() {
@Override
public void onSuccess(Account inserted) {
// connectionCenter.createConnection(account);
signinSuccessful.setValue(true);
}
@Override
public void onError(Throwable e) {
Log.e("Mercury", "Could not insert new Account data.", e);
}
});
public String getErrorMessage() {
return message;
}
}
}

View File

@ -8,7 +8,7 @@ sourceSets {
dependencies {
api project(':entity') // Entities
api project(':entity')
// Smack
// Not all of those are needed, but it may be a good idea to define those versions explicitly
@ -37,6 +37,7 @@ dependencies {
// JUnit for testing
testImplementation "junit:junit:$junitVersion"
compile project(path: ':data')
}
sourceCompatibility = "8"

View File

@ -4,8 +4,8 @@ import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smack.xml.XmlPullParser;
import org.jivesoftware.smackx.caps.cache.EntityCapsPersistentCache;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.mercury_im.messenger.xmpp.model.EntityCapsModel;
import org.mercury_im.messenger.xmpp.repository.EntityCapsRepository;
import org.mercury_im.messenger.data.model.EntityCapsModel;
import org.mercury_im.messenger.data.repository.XmppEntityCapsRepository;
import java.io.StringReader;
@ -23,13 +23,13 @@ public class EntityCapsStore implements EntityCapsPersistentCache {
private static final Logger LOGGER = Logger.getLogger(EntityCapsStore.class.getName());
private final EntityCapsRepository entityCapsRepository;
private final XmppEntityCapsRepository entityCapsRepository;
private final Map<String, DiscoverInfo> discoverInfoMap = new HashMap<>();
private final CompositeDisposable disposable = new CompositeDisposable();
@Inject
public EntityCapsStore(EntityCapsRepository entityCapsRepository) {
public EntityCapsStore(XmppEntityCapsRepository entityCapsRepository) {
this.entityCapsRepository = entityCapsRepository;
populateFromDatabase();
}

View File

@ -10,7 +10,7 @@ import org.mercury_im.messenger.data.repository.GroupChatRepository;
import org.mercury_im.messenger.data.repository.MessageRepository;
import org.mercury_im.messenger.data.repository.PeerRepository;
import org.mercury_im.messenger.data.repository.DirectChatRepository;
import org.mercury_im.messenger.data.repository.EntityCapsRepository;
import org.mercury_im.messenger.data.repository.XmppEntityCapsRepository;
import org.mercury_im.messenger.data.repository.Repositories;
import org.mercury_im.messenger.data.repository.XmppAccountRepository;
import org.mercury_im.messenger.data.repository.XmppDirectChatRepository;
@ -89,11 +89,11 @@ public class RepositoryModule {
@Provides
@Singleton
static EntityCapsRepository provideCapsRepository(
static XmppEntityCapsRepository provideCapsRepository(
ReactiveEntityStore<Persistable> data,
@Named(value = ThreadUtils.SCHEDULER_IO) Scheduler ioScheduler,
@Named(value = ThreadUtils.SCHEDULER_UI) Scheduler uiScheduler) {
return new EntityCapsRepository(data, ioScheduler, uiScheduler);
return new XmppEntityCapsRepository(data, ioScheduler, uiScheduler);
}
@Provides

View File

@ -3,7 +3,6 @@ package org.mercury_im.messenger.data.mapping;
import org.mercury_im.messenger.data.model.AccountModel;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.entity.IAccount;
import org.mercury_im.messenger.entity.PasswordAuthentication;
import javax.inject.Inject;
@ -27,7 +26,7 @@ public class AccountMapping extends AbstractMapping<Account, AccountModel> {
@Override
public AccountModel mapToModel(Account entity, AccountModel model) {
model.setAddress(entity.getAddress());
model.setPassword(entity.getAuthentication().getPassword());
model.setPassword(entity.getPassword());
model.setEnabled(entity.isEnabled());
return model;
@ -37,7 +36,7 @@ public class AccountMapping extends AbstractMapping<Account, AccountModel> {
public Account mapToEntity(AccountModel model, Account entity) {
entity.setId(model.getId());
entity.setAddress(model.getAddress());
entity.setAuthentication(new PasswordAuthentication(model.getPassword()));
entity.setPassword(model.getPassword());
entity.setEnabled(model.isEnabled());
return entity;

View File

@ -9,10 +9,10 @@ import io.reactivex.Scheduler;
import io.requery.Persistable;
import io.requery.reactivex.ReactiveEntityStore;
public class EntityCapsRepository extends RequeryRepository {
public class XmppEntityCapsRepository extends RequeryRepository {
@Inject
public EntityCapsRepository(
public XmppEntityCapsRepository(
ReactiveEntityStore<Persistable> data,
@Named(value = ThreadUtils.SCHEDULER_IO) Scheduler subscriberScheduler,
@Named(value = ThreadUtils.SCHEDULER_UI) Scheduler observerScheduler) {

View File

@ -74,8 +74,8 @@ public class XmppGroupChatRepository
@Override
public Single<GroupChat> getOrCreateGroupChat(Account account, String roomAddress) {
return getGroupChatByRoomAddress(account, roomAddress)
.switchIfEmpty(Single
.just((GroupChat) new IGroupChat() {
.switchIfEmpty(
Single.just((GroupChat) new IGroupChat() {
{
setAccount(account);
setRoomAddress(roomAddress);

View File

@ -6,21 +6,6 @@ import org.mercury_im.messenger.data.di.InMemoryDatabaseComponent;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.entity.IAccount;
import org.mercury_im.messenger.entity.PasswordAuthentication;
import org.mercury_im.messenger.entity.chat.DirectChat;
import org.mercury_im.messenger.entity.chat.IDirectChat;
import org.mercury_im.messenger.entity.contact.IPeer;
import org.mercury_im.messenger.entity.contact.Peer;
import org.mercury_im.messenger.entity.message.IMessage;
import org.mercury_im.messenger.entity.message.IPayloadContainer;
import org.mercury_im.messenger.entity.message.Message;
import org.mercury_im.messenger.entity.message.PayloadContainer;
import org.mercury_im.messenger.entity.message.content.TextPayload;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
@ -28,8 +13,6 @@ import io.reactivex.disposables.CompositeDisposable;
import io.requery.Persistable;
import io.requery.reactivex.ReactiveEntityStore;
import static junit.framework.TestCase.assertEquals;
public class AccountRepositoryTest {
@Inject

1
domain/.gitignore vendored
View File

@ -1 +1,2 @@
/build
testcredentials.properties

View File

@ -4,6 +4,16 @@ dependencies {
implementation project(':entity')
// Smack
// Not all of those are needed, but it may be a good idea to define those versions explicitly
api "org.igniterealtime.smack:smack-core:$smackCoreVersion"
api "org.igniterealtime.smack:smack-experimental:$smackExperimentalVersion"
api "org.igniterealtime.smack:smack-extensions:$smackExtensionsVersion"
api "org.igniterealtime.smack:smack-im:$smackImVersion"
api "org.igniterealtime.smack:smack-tcp:$smackTcpVersion"
testImplementation "org.igniterealtime.smack:smack-java7:$smackJava7Version"
// RxJava2
implementation "io.reactivex.rxjava2:rxjava:$rxJava2Version"

View File

@ -1,6 +1,6 @@
package org.mercury_im.messenger;
import org.mercury_im.messenger.transport.listener.IncomingDirectMessageListener;
import org.mercury_im.messenger.listener.IncomingDirectMessageListener;
import org.mercury_im.messenger.entity.chat.Chat;
import org.mercury_im.messenger.entity.message.Message;

View File

@ -2,7 +2,8 @@ package org.mercury_im.messenger;
import org.mercury_im.messenger.data.repository.Repositories;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.transport.connection.ConnectionMethod;
import org.mercury_im.messenger.usecase.AddAccount;
import org.mercury_im.messenger.xmpp.MercuryConnection;
import java.util.HashMap;
import java.util.Map;
@ -13,28 +14,23 @@ public class Messenger {
public static final String TAG = "MercuryIM";
private final Map<Long, ConnectionMethod> connections = new HashMap<>();
private final Repositories repositories;
private final Map<Long, MercuryConnection> connections = new HashMap<>();
private Repositories repositories;
@Inject
public Messenger(Repositories repositories) {
this.repositories = repositories;
}
public void addConnection(ConnectionMethod connection) {
public void addConnection(MercuryConnection connection) {
connections.put(connection.getAccount().getId(), connection);
}
public ConnectionMethod getConnection(Account account) {
public MercuryConnection getConnection(Account account) {
return connections.get(account.getId());
}
public void appInUse() {
}
public void appInBackground() {
public AddAccount addAccount() {
return new AddAccount(repositories.getAccountRepository(), this);
}
}

View File

@ -1,23 +0,0 @@
package org.mercury_im.messenger.di.module;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.data.repository.Repositories;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class MercuryModule {
/*
@Provides
@Singleton
static Messenger provideMessenger(Repositories repositories) {
return new Messenger(repositories);
}
*/
}

View File

@ -0,0 +1,8 @@
package org.mercury_im.messenger.exception;
public class IllegalUsernameException extends RuntimeException {
public IllegalUsernameException(Throwable cause) {
super(cause);
}
}

View File

@ -1,4 +1,4 @@
package org.mercury_im.messenger.transport.listener;
package org.mercury_im.messenger.listener;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.entity.chat.DirectChat;

View File

@ -1,4 +1,4 @@
package org.mercury_im.messenger.transport.listener;
package org.mercury_im.messenger.listener;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.entity.chat.GroupChat;

View File

@ -1,4 +1,4 @@
package org.mercury_im.messenger.transport.listener;
package org.mercury_im.messenger.listener;
import org.mercury_im.messenger.entity.chat.Chat;
import org.mercury_im.messenger.entity.event.TypingEvent;

View File

@ -1,27 +0,0 @@
package org.mercury_im.messenger.transport;
import org.mercury_im.messenger.entity.Account;
public class AccountChangedEvent {
private final Account account;
private final EventType eventType;
public AccountChangedEvent(Account account, EventType eventType) {
this.account = account;
this.eventType = eventType;
}
public Account getAccount() {
return account;
}
public EventType getEventType() {
return eventType;
}
public enum EventType {
enabled,
disabled
}
}

View File

@ -1,77 +0,0 @@
package org.mercury_im.messenger.transport;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.data.repository.AccountRepository;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.util.DiffUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import io.reactivex.disposables.CompositeDisposable;
public class AccountChangedHandler {
private final Messenger messenger;
private final AccountRepository accountRepository;
private final Map<Long, Account> accounts = new HashMap<>();
private final CompositeDisposable disposable = new CompositeDisposable();
@Inject
public AccountChangedHandler(Messenger messenger, AccountRepository accountRepository) {
this.messenger = messenger;
this.accountRepository = accountRepository;
//disposable.add(accountRepository.observeAllAccounts()
// .scan(new ArrayList<>(), this::calculateChangeEvents)
// .subscribe(this::onAccountsChanged));
}
private HashMap<Long, Account> calculateChangeEvents(List<Account> accounts, List<Account> newAccounts) {
DiffUtil.Diff<Account> diff = DiffUtil.diff(accounts, newAccounts);
return null;
}
private AccountChangedEvent calculateEvent(Account previous, Account next) {
if (previous == null && next == null) {
throw new IllegalArgumentException("Not both accounts can be null at the same time!");
}
if (previous == null) {
if (next.isEnabled()) {
return new AccountChangedEvent(next, AccountChangedEvent.EventType.enabled);
} else {
return null;
}
}
if (next == null) {
if (previous.isEnabled()) {
return new AccountChangedEvent(previous, AccountChangedEvent.EventType.disabled);
} else {
return null;
}
}
if (previous.isEnabled()) {
if (next.isEnabled()) {
return null;
} else {
return new AccountChangedEvent(next, AccountChangedEvent.EventType.disabled);
}
} else {
if (next.isEnabled()) {
return new AccountChangedEvent(next, AccountChangedEvent.EventType.enabled);
} else {
return null;
}
}
}
private void onAccountsChanged(List<Account> accounts, List<Account> newAccounts) {
DiffUtil.Diff<Account> diff = DiffUtil.diff(accounts, newAccounts);
}
}

View File

@ -1,22 +0,0 @@
package org.mercury_im.messenger.transport;
public enum ConnectionType {
// Smack Connection Types from module transport_xmpp.
/**
* Underlying connection is a Smack XMPPTCPConnection.
*/
SMACK_TCP,
/**
* Underlying connection is a Smack XMPPBOSHConnection.
* @deprecated Not yet implemented.
*/
SMACK_BOSH,
/**
* Underlying connection is a Smack XMPPWebsocketConnection.
* @deprecated Not yet implemented.
*/
SMACK_WEBSOCKETS,
;
}

View File

@ -1,26 +0,0 @@
package org.mercury_im.messenger.transport.connection;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.entity.Account;
public abstract class AbstractConnectionMethod
implements ConnectionMethod {
protected final Account account;
protected final Messenger messenger;
public AbstractConnectionMethod(Account account, Messenger messenger) {
this.account = account;
this.messenger = messenger;
}
@Override
public Account getAccount() {
return account;
}
@Override
public Messenger getMessenger() {
return messenger;
}
}

View File

@ -1,13 +0,0 @@
package org.mercury_im.messenger.transport.connection;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.entity.Account;
public interface ConnectionFactory<
CM extends ConnectionMethod> {
Messenger getMessenger();
CM provideConnection(Account account);
}

View File

@ -1,18 +0,0 @@
package org.mercury_im.messenger.transport.connection;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.transport.ConnectionType;
import org.mercury_im.messenger.entity.Account;
import io.reactivex.Completable;
public interface ConnectionMethod {
Account getAccount();
Messenger getMessenger();
Completable connect();
ConnectionType getConnectionType();
}

View File

@ -1,6 +0,0 @@
package org.mercury_im.messenger.transport.connection.exception;
public class ConnectionFailedException extends Exception {
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,132 @@
package org.mercury_im.messenger.usecase;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.sasl.SASLErrorException;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.data.repository.AccountRepository;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.entity.IAccount;
import org.mercury_im.messenger.exception.IllegalUsernameException;
import org.mercury_im.messenger.xmpp.MercuryConnection;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.reactivex.Completable;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
public class AddAccount {
private static final Logger LOGGER = Logger.getLogger(AddAccount.class.getName());
private String username;
private String server;
private BareJid address;
private String password;
private Account account;
private final AccountRepository accountRepository;
private final Messenger messenger;
public AddAccount(AccountRepository accountRepository, Messenger messenger) {
this.accountRepository = accountRepository;
this.messenger = messenger;
}
public void setAddress(String address) {
try {
this.address = JidCreate.entityBareFrom(address);
} catch (XmppStringprepException e) {
this.address = null;
throw new IllegalUsernameException(e);
}
}
public boolean isAddressSet() {
return address != null;
}
public void setPassword(String password) {
this.password = password;
}
public boolean isPasswordSet() {
return password != null;
}
public Single<Account> insertAccountIntoDatabase() {
Account account = new IAccount();
account.setAddress(address.asUnescapedString());
account.setPassword(password);
return accountRepository.insertAccount(account).doAfterSuccess(this::setAccount);
}
private void setAccount(Account account) {
this.account = account;
}
public Single<ConnectionResult> enableAccountAndLogin(Account account) {
return enableAccount(account).andThen(login(account));
}
private Completable enableAccount(Account account) {
account.setEnabled(true);
return accountRepository.upsertAccount(account)
.doAfterSuccess(this::setAccount)
.ignoreElement();
}
private Single<ConnectionResult> login(Account account) {
return Single.fromCallable(() -> {
MercuryConnection connection = getOrCreateConnection(account);
return authenticateIfNecessary(connection);
}).subscribeOn(Schedulers.io());
}
private MercuryConnection getOrCreateConnection(Account account) {
MercuryConnection connection = messenger.getConnection(account);
if (connection == null) {
connection = new MercuryConnection(account);
messenger.addConnection(connection);
}
return connection;
}
private ConnectionResult authenticateIfNecessary(MercuryConnection connection) {
try {
doAuthenticateIfNecessary(connection);
return ConnectionResult.success;
} catch (SASLErrorException e) {
LOGGER.log(Level.WARNING, "SASL Error while connecting to account " + account.getAddress(), e);
return ConnectionResult.credential_error;
} catch (SmackException.ConnectionException e) {
LOGGER.log(Level.WARNING, "Connectivity error while connecting to account " + account.getAddress(), e);
return ConnectionResult.server_error;
}
catch (IOException | XMPPException | SmackException | InterruptedException e) {
LOGGER.log(Level.WARNING, "Error connecting to account " + account.getAddress(), e);
return ConnectionResult.other_error;
}
}
private void doAuthenticateIfNecessary(MercuryConnection connection)
throws InterruptedException, XMPPException, SmackException, IOException {
if (!connection.getConnection().isAuthenticated()) {
((AbstractXMPPConnection) connection.getConnection()).connect().login();
}
}
public enum ConnectionResult {
success,
credential_error,
server_error,
other_error
}
}

View File

@ -0,0 +1,34 @@
package org.mercury_im.messenger.xmpp;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jxmpp.stringprep.XmppStringprepException;
import org.mercury_im.messenger.entity.Account;
public class MercuryConnection {
private final Account account;
private XMPPConnection connection;
public MercuryConnection(Account account) {
this.account = account;
try {
this.connection = new XMPPTCPConnection(account.getAddress(), account.getPassword());
} catch (XmppStringprepException e) {
throw new AssertionError("Account has invalid address: " + account.getAddress(), e);
}
}
public Account getAccount() {
return account;
}
public XMPPConnection getConnection() {
return connection;
}
public void ensureAuthenticated() {
}
}

View File

@ -1,10 +1,9 @@
package org.mercury_im.messenger.domain.xmpp;
package org.mercury_im.messenger.xmpp;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.chat2.Chat;
import org.jivesoftware.smack.chat2.ChatManager;
import org.jivesoftware.smack.chat2.IncomingChatMessageListener;
import org.jivesoftware.smackx.avatar.element.MetadataExtension;
import org.jivesoftware.smackx.sid.element.OriginIdElement;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate;
@ -20,9 +19,7 @@ import org.mercury_im.messenger.entity.chat.DirectChat;
import org.mercury_im.messenger.entity.message.IMessage;
import org.mercury_im.messenger.entity.message.IMessageMetadata;
import org.mercury_im.messenger.entity.message.Message;
import org.mercury_im.messenger.entity.message.MessageMetadata;
import org.mercury_im.messenger.transport.listener.IncomingDirectMessageListener;
import org.mercury_im.messenger.transport.xmpp.XmppTcpConnectionMethod;
import org.mercury_im.messenger.listener.IncomingDirectMessageListener;
import java.util.LinkedHashSet;
import java.util.Set;
@ -56,8 +53,7 @@ public class XmppDirectMessageCenter
this.directChatRepository = repositories.getDirectChatRepository();
this.messageRepository = repositories.getMessageRepository();
XMPPConnection connection = ((XmppTcpConnectionMethod) getMessenger()
.getConnection(account)).getConnection();
XMPPConnection connection = getMessenger().getConnection(account).getConnection();
ChatManager.getInstanceFor(connection).addIncomingListener(this);
}
@ -102,8 +98,8 @@ public class XmppDirectMessageCenter
}
protected ChatManager getChatManager(DirectChat chat) {
XmppTcpConnectionMethod connectionMethod = (XmppTcpConnectionMethod) getMessenger().getConnection(chat.getAccount());
return ChatManager.getInstanceFor(connectionMethod.getConnection());
MercuryConnection mercuryConnection = getMessenger().getConnection(chat.getAccount());
return ChatManager.getInstanceFor(mercuryConnection.getConnection());
}
@Override

View File

@ -0,0 +1,91 @@
package org.mercury_im.messenger.learning_tests.smack;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.sasl.SASLErrorException;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.util.stringencoder.Base64;
import org.jivesoftware.smack.util.stringencoder.java7.Java7Base64Encoder;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.junit.Assume.assumeTrue;
/**
* Learning Test to study Smacks behavior during connection process.
*/
public class XmppConnectionLoginBehaviorTest {
public static final String CREDENTIALS_PROPERTIES = "testcredentials.properties";
private static final Logger LOGGER = Logger.getLogger(XmppConnectionLoginBehaviorTest.class.getName());
private static Properties testCredentials = null;
@BeforeClass
public static void readProperties() {
Properties properties = new Properties();
File propertiesFile = new File(CREDENTIALS_PROPERTIES);
if (!propertiesFile.exists() || !propertiesFile.isFile()) {
LOGGER.log(Level.WARNING, "Cannot find file domain/testcredentials.properties. Some tests will be skipped.");
return;
}
try(FileReader reader = new FileReader(propertiesFile)) {
properties.load(reader);
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Error reading properties file testcredentials.properties.", e);
return;
}
testCredentials = properties;
Base64.setEncoder(Java7Base64Encoder.getInstance());
}
/*
* Connecting to an invalid host causes {@link org.jivesoftware.smack.SmackException.ConnectionException}
* to be thrown.
*/
@Test(expected = SmackException.ConnectionException.class)
public void invalidHostConnectionTest() throws IOException, InterruptedException, XMPPException, SmackException {
ignoreIfNoCredentials();
new XMPPTCPConnection(
testCredentials.getProperty("invalidHostUsername"),
testCredentials.getProperty("invalidHostPassword"))
.connect().login();
}
/*
* Connecting with invalid user causes {@link SASLErrorException} to be thrown.
*/
@Test(expected = SASLErrorException.class)
public void invalidUserConnectionTest() throws IOException, InterruptedException, XMPPException, SmackException {
ignoreIfNoCredentials();
new XMPPTCPConnection(
testCredentials.getProperty("invalidUserUsername"),
testCredentials.getProperty("invalidUserPassword"))
.connect().login();
}
/*
* Connecting with invalid password causes {@link SASLErrorException} to be thrown.
*/
@Test(expected = SASLErrorException.class)
public void invalidPasswordConnectionTest() throws IOException, InterruptedException, XMPPException, SmackException {
ignoreIfNoCredentials();
new XMPPTCPConnection(
testCredentials.getProperty("invalidPasswordUsername"),
testCredentials.getProperty("invalidPasswordPassword"))
.connect().login();
}
private void ignoreIfNoCredentials() {
assumeTrue("Test ignored as domain/testcredentials.properties file not found.", testCredentials != null);
}
}

View File

@ -0,0 +1,11 @@
#Server must not exist
invalidHostUsername=invalid@example.com
invalidHostPassword=invalid123
#User must not exist on a server that is reachable
invalidUserUsername=
invalidPassword=
#User must exists, but password must be invalid
invalidPasswordUsername=
invalidPasswordPassword=

View File

@ -15,8 +15,14 @@ public interface Account {
String getAddress();
void setPassword(String password);
String getPassword();
@Deprecated
void setAuthentication(AuthMethod authentication);
@Deprecated
AuthMethod getAuthentication();
void setEnabled(boolean enabled);

View File

@ -4,6 +4,7 @@ public class IAccount implements Account {
protected long id;
protected String address;
protected String password;
protected AuthMethod authentication;
protected boolean enabled;
@ -28,6 +29,16 @@ public class IAccount implements Account {
return address;
}
@Override
public void setPassword(String password) {
this.password = password;
}
@Override
public String getPassword() {
return password;
}
@Override
public void setAuthentication(AuthMethod authentication) {
this.authentication = authentication;

View File

@ -1,9 +1,7 @@
include ':entity',
':data',
':domain',
':xmpp',
':app',
':core-old',
':view_entity'
':core-old'
includeBuild 'libs/Smack'

View File

@ -1 +0,0 @@
/build

View File

@ -1,8 +0,0 @@
apply plugin: 'java-library'
dependencies {
implementation project(':entity')
}
sourceCompatibility = "8"
targetCompatibility = "8"

View File

@ -1,134 +0,0 @@
package org.mercury_im.messenger.view.entity;
import org.mercury_im.messenger.entity.contact.SubscriptionMode;
import org.mercury_im.messenger.view.entity.definition.InterlocutorViewEntity;
public class ViewInterlocutor<A> implements InterlocutorViewEntity {
private final String name;
private final String address;
private final String accountAddress;
private final SubscriptionMode subscriptionMode;
private final String lastActivity;
private final boolean isTyping;
private final String status;
private final A avatar;
private ViewInterlocutor(String name,
String address,
String accountAddress,
SubscriptionMode subscriptionMode,
String lastActivity,
boolean isTyping,
String status,
A avatar) {
this.name = name;
this.address = address;
this.accountAddress = accountAddress;
this.subscriptionMode = subscriptionMode;
this.lastActivity = lastActivity;
this.isTyping = isTyping;
this.status = status;
this.avatar = avatar;
}
@Override
public String getName() {
return name;
}
@Override
public String getAddress() {
return address;
}
@Override
public String getAccountAddress() {
return accountAddress;
}
@Override
public SubscriptionMode getSubscriptionMode() {
return subscriptionMode;
}
@Override
public String getLastActivity() {
return lastActivity;
}
@Override
public boolean isTyping() {
return isTyping;
}
@Override
public String getStatus() {
return status;
}
public A getAvatar() {
return avatar;
}
public static <A> Builder<A> builder() {
return new Builder<>();
}
private static class Builder<A> {
private String name;
private String address;
private String accountAddress;
private SubscriptionMode subscriptionMode;
private String lastActivity;
private boolean isTyping;
private String status;
private A avatar;
public Builder setName(String name) {
this.name = name;
return this;
}
public Builder setAddress(String address) {
this.address = address;
return this;
}
public Builder setAccountAddress(String accountAddress) {
this.accountAddress = accountAddress;
return this;
}
public Builder setSubscriptionMode(SubscriptionMode subscriptionMode) {
this.subscriptionMode = subscriptionMode;
return this;
}
public Builder setLastActivity(String lastActivity) {
this.lastActivity = lastActivity;
return this;
}
public Builder setIsTyping(boolean isTyping) {
this.isTyping = isTyping;
return this;
}
public Builder setStatus(String status) {
this.status = status;
return this;
}
public Builder setAvatar(A avatar) {
this.avatar = avatar;
return this;
}
public ViewInterlocutor<A> build() {
return new ViewInterlocutor<>(name, address, accountAddress, subscriptionMode,
lastActivity, isTyping, status, avatar);
}
}
}

View File

@ -1,20 +0,0 @@
package org.mercury_im.messenger.view.entity.definition;
import org.mercury_im.messenger.entity.contact.SubscriptionMode;
public interface InterlocutorViewEntity {
String getName();
String getAddress();
String getAccountAddress();
SubscriptionMode getSubscriptionMode();
String getLastActivity();
boolean isTyping();
String getStatus();
}

1
xmpp/.gitignore vendored
View File

@ -1 +0,0 @@
/build

View File

@ -1,25 +0,0 @@
apply plugin: 'java-library'
dependencies {
implementation project(":entity")
implementation project(':domain')
// RxJava2
implementation "io.reactivex.rxjava2:rxjava:$rxJava2Version"
// Dagger 2 for dependency injection
implementation "com.google.dagger:dagger:$daggerVersion"
annotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion"
testAnnotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion"
// Smack
// Not all of those are needed, but it may be a good idea to define those versions explicitly
api "org.igniterealtime.smack:smack-core:$smackCoreVersion"
api "org.igniterealtime.smack:smack-experimental:$smackExperimentalVersion"
api "org.igniterealtime.smack:smack-extensions:$smackExtensionsVersion"
api "org.igniterealtime.smack:smack-im:$smackImVersion"
api "org.igniterealtime.smack:smack-tcp:$smackTcpVersion"
}
sourceCompatibility = "8"
targetCompatibility = "8"

View File

@ -1,30 +0,0 @@
package org.mercury_im.messenger.transport.xmpp;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.XMPPConnection;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.transport.connection.ConnectionFactory;
import org.mercury_im.messenger.entity.Account;
public abstract class XmppConnectionFactory<CF extends ConnectionConfiguration>
implements ConnectionFactory<XmppTcpConnectionMethod> {
protected final Messenger messenger;
public XmppConnectionFactory(Messenger messenger) {
this.messenger = messenger;
}
public Messenger getMessenger() {
return messenger;
}
@Override
public XmppTcpConnectionMethod provideConnection(Account account) {
return new XmppTcpConnectionMethod(account, getMessenger(), createXmppConnection(getConfiguration(account)));
}
protected abstract CF getConfiguration(Account account);
protected abstract XMPPConnection createXmppConnection(CF connectionConfiguration);
}

View File

@ -1,36 +0,0 @@
package org.mercury_im.messenger.transport.xmpp;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.entity.PasswordAuthentication;
public class XmppTcpConnectionFactory extends XmppConnectionFactory<XMPPTCPConnectionConfiguration> {
public XmppTcpConnectionFactory(Messenger messenger) {
super(messenger);
}
@Override
protected XMPPTCPConnectionConfiguration getConfiguration(Account account) {
XMPPTCPConnectionConfiguration.Builder configBuilder = XMPPTCPConnectionConfiguration.builder();
configBuilder.setConnectTimeout(20 * 1000);
if (account.getAuthentication() instanceof PasswordAuthentication) {
PasswordAuthentication authPassword = (PasswordAuthentication) account.getAuthentication();
EntityBareJid accountAddress = JidCreate.entityBareFromOrThrowUnchecked(account.getAddress());
configBuilder.setXmppAddressAndPassword(accountAddress, authPassword.getPassword());
}
return configBuilder.build();
}
@Override
protected XMPPConnection createXmppConnection(XMPPTCPConnectionConfiguration configuration) {
return new XMPPTCPConnection(configuration);
}
}

View File

@ -1,59 +0,0 @@
package org.mercury_im.messenger.transport.xmpp;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.XMPPConnection;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.transport.ConnectionType;
import org.mercury_im.messenger.transport.connection.AbstractConnectionMethod;
import org.mercury_im.messenger.entity.Account;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.reactivex.Completable;
public class XmppTcpConnectionMethod extends AbstractConnectionMethod {
private static final Logger LOGGER = Logger.getLogger(XmppTcpConnectionMethod.class.getName());
private XMPPConnection connection;
public XmppTcpConnectionMethod(Account account, Messenger messenger, XMPPConnection connection) {
super(account, messenger);
this.connection = connection;
}
@Override
public Completable connect() {
if (connection.isConnected()) {
return Completable.complete();
}
return Completable.fromAction(
() -> {
AbstractXMPPConnection con = (AbstractXMPPConnection) connection;
try {
con.connect();
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Exception while connecting to XMPP account " + account.getId(), e);
throw e;
}
try {
con.login();
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Exception while logging into XMPP account " + account.getId(), e);
throw e;
}
});
}
public XMPPConnection getConnection() {
return connection;
}
@Override
public ConnectionType getConnectionType() {
return ConnectionType.SMACK_TCP;
}
}