Merge CleanArchitecture

This commit is contained in:
Paul Schaub 2020-01-06 04:18:22 +01:00
commit 576b300732
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
202 changed files with 6577 additions and 2076 deletions

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
</project>

View File

@ -1,91 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<compositeConfiguration>
<compositeBuild compositeDefinitionSource="SCRIPT">
<builds>
<build path="$PROJECT_DIR$/libs/Smack" name="Smack">
<projects>
<project path="$PROJECT_DIR$/libs/Smack" />
<project path="$PROJECT_DIR$/libs/Smack/smack-android" />
<project path="$PROJECT_DIR$/libs/Smack/smack-android-extensions" />
<project path="$PROJECT_DIR$/libs/Smack/smack-bosh" />
<project path="$PROJECT_DIR$/libs/Smack/smack-compression-jzlib" />
<project path="$PROJECT_DIR$/libs/Smack/smack-core" />
<project path="$PROJECT_DIR$/libs/Smack/smack-debug" />
<project path="$PROJECT_DIR$/libs/Smack/smack-debug-slf4j" />
<project path="$PROJECT_DIR$/libs/Smack/smack-experimental" />
<project path="$PROJECT_DIR$/libs/Smack/smack-extensions" />
<project path="$PROJECT_DIR$/libs/Smack/smack-im" />
<project path="$PROJECT_DIR$/libs/Smack/smack-integration-test" />
<project path="$PROJECT_DIR$/libs/Smack/smack-java7" />
<project path="$PROJECT_DIR$/libs/Smack/smack-jingle-old" />
<project path="$PROJECT_DIR$/libs/Smack/smack-legacy" />
<project path="$PROJECT_DIR$/libs/Smack/smack-omemo" />
<project path="$PROJECT_DIR$/libs/Smack/smack-omemo-signal" />
<project path="$PROJECT_DIR$/libs/Smack/smack-omemo-signal-integration-test" />
<project path="$PROJECT_DIR$/libs/Smack/smack-openpgp" />
<project path="$PROJECT_DIR$/libs/Smack/smack-repl" />
<project path="$PROJECT_DIR$/libs/Smack/smack-resolver-dnsjava" />
<project path="$PROJECT_DIR$/libs/Smack/smack-resolver-javax" />
<project path="$PROJECT_DIR$/libs/Smack/smack-resolver-minidns" />
<project path="$PROJECT_DIR$/libs/Smack/smack-resolver-minidns-dox" />
<project path="$PROJECT_DIR$/libs/Smack/smack-sasl-javax" />
<project path="$PROJECT_DIR$/libs/Smack/smack-sasl-provided" />
<project path="$PROJECT_DIR$/libs/Smack/smack-tcp" />
<project path="$PROJECT_DIR$/libs/Smack/smack-xmlparser" />
<project path="$PROJECT_DIR$/libs/Smack/smack-xmlparser-stax" />
<project path="$PROJECT_DIR$/libs/Smack/smack-xmlparser-xpp3" />
</projects>
</build>
</builds>
</compositeBuild>
</compositeConfiguration>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/core" />
<option value="$PROJECT_DIR$/libs/Smack" />
<option value="$PROJECT_DIR$/libs/Smack/smack-android" />
<option value="$PROJECT_DIR$/libs/Smack/smack-android-extensions" />
<option value="$PROJECT_DIR$/libs/Smack/smack-bosh" />
<option value="$PROJECT_DIR$/libs/Smack/smack-compression-jzlib" />
<option value="$PROJECT_DIR$/libs/Smack/smack-core" />
<option value="$PROJECT_DIR$/libs/Smack/smack-debug" />
<option value="$PROJECT_DIR$/libs/Smack/smack-debug-slf4j" />
<option value="$PROJECT_DIR$/libs/Smack/smack-experimental" />
<option value="$PROJECT_DIR$/libs/Smack/smack-extensions" />
<option value="$PROJECT_DIR$/libs/Smack/smack-im" />
<option value="$PROJECT_DIR$/libs/Smack/smack-integration-test" />
<option value="$PROJECT_DIR$/libs/Smack/smack-java7" />
<option value="$PROJECT_DIR$/libs/Smack/smack-jingle-old" />
<option value="$PROJECT_DIR$/libs/Smack/smack-legacy" />
<option value="$PROJECT_DIR$/libs/Smack/smack-omemo" />
<option value="$PROJECT_DIR$/libs/Smack/smack-omemo-signal" />
<option value="$PROJECT_DIR$/libs/Smack/smack-omemo-signal-integration-test" />
<option value="$PROJECT_DIR$/libs/Smack/smack-openpgp" />
<option value="$PROJECT_DIR$/libs/Smack/smack-repl" />
<option value="$PROJECT_DIR$/libs/Smack/smack-resolver-dnsjava" />
<option value="$PROJECT_DIR$/libs/Smack/smack-resolver-javax" />
<option value="$PROJECT_DIR$/libs/Smack/smack-resolver-minidns" />
<option value="$PROJECT_DIR$/libs/Smack/smack-resolver-minidns-dox" />
<option value="$PROJECT_DIR$/libs/Smack/smack-sasl-javax" />
<option value="$PROJECT_DIR$/libs/Smack/smack-sasl-provided" />
<option value="$PROJECT_DIR$/libs/Smack/smack-tcp" />
<option value="$PROJECT_DIR$/libs/Smack/smack-xmlparser" />
<option value="$PROJECT_DIR$/libs/Smack/smack-xmlparser-stax" />
<option value="$PROJECT_DIR$/libs/Smack/smack-xmlparser-xpp3" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="testRunner" value="PLATFORM" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/classes" />
</component>
</project>

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libs/Smack" vcs="Git" />
</component>
</project>

View File

@ -2,10 +2,11 @@
## Used Design Methods:
* The app is developed using the MVVM (Model View Viewmodel) pattern using LifeCycle aware ViewModels
* Components are wired together using Dependency Injection (DI) with Dagger 2
* Data is persisted using Googles Room database library
* UI is notified by updates to the data through the use of LiveData
* Mercury IM's development follows architectural principles know from
[Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html).
* The app is developed using the MVVM (Model View Viewmodel) pattern
* Components are wired together using Dependency Injection (DI) with [Dagger 2](https://dagger.dev)
* Data is persisted using the [requery](https://github.com/requery/requery) ORM framework
## Building
@ -20,5 +21,5 @@ gradle assembleDebug
* I want to develop, but lots of `org.jivesoftware.smackx.*` classes cannot be found!
* You forgot to type `git submodule init && git submodule update` as mentioned above
* I'm missing `org.mercury_im.messenger.persistence.requery.*` classes???
* In Android Studio select the `persistence-requery` module and then click "Build -> Make Module 'persistence-requery'".
* It looks like I'm missing `org.mercury_im.messenger.data.*` classes???
* In Android Studio select the `data` module and then click "Build -> Make Module 'data'".

View File

@ -23,7 +23,7 @@ android {
lintOptions {
disable 'GoogleAppIndexingWarning', 'AllowBackup'
// Warn about invalid packages instead of failing
// Warn about invalidUsername packages instead of failing
warning 'InvalidPackage'
}
@ -73,18 +73,23 @@ check.configure {
dependencies {
// Depend on the core project for XMPP related stuff
implementation project(':core')
implementation project(":entity")
implementation project(":domain")
implementation project(":data")
implementation "io.requery:requery-android:$requeryVersion"
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.41'
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.61'
// Dagger 2 for dependency injection
implementation "com.google.dagger:dagger:$daggerVersion"
annotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion"
compileOnly 'org.projectlombok:lombok:1.18.10'
annotationProcessor 'org.projectlombok:lombok:1.18.10'
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion"
annotationProcessor "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
// Android extension for rxJava
api "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
@ -95,12 +100,12 @@ dependencies {
// support libraries
implementation "androidx.appcompat:appcompat:$appCompatVersion"
implementation 'com.google.android.material:material:1.1.0-beta01'
implementation 'com.google.android.material:material:1.2.0-alpha03'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
// circular image viewer for avatars
implementation 'de.hdodenhof:circleimageview:3.0.1'

View File

@ -9,13 +9,13 @@ import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.jxmpp.jid.impl.JidCreate;
import org.mercury_im.messenger.persistence.requery.entity.AccountModel;
import org.mercury_im.messenger.persistence.requery.entity.ChatModel;
import org.mercury_im.messenger.persistence.requery.entity.ContactModel;
import org.mercury_im.messenger.persistence.requery.entity.EntityModel;
import org.mercury_im.messenger.persistence.requery.entity.LastReadChatMessageRelation;
import org.mercury_im.messenger.persistence.requery.entity.MessageModel;
import org.mercury_im.messenger.persistence.requery.entity.Models;
import org.mercury_im.messenger.xmpp.requery.entity.AccountModel;
import org.mercury_im.messenger.xmpp.requery.entity.ChatModel;
import org.mercury_im.messenger.xmpp.requery.entity.ContactModel;
import org.mercury_im.messenger.xmpp.requery.entity.EntityModel;
import org.mercury_im.messenger.xmpp.requery.entity.LastReadChatMessageRelation;
import org.mercury_im.messenger.xmpp.requery.entity.MessageModel;
import org.mercury_im.messenger.xmpp.requery.entity.Models;
import org.mercury_im.messenger.core.requery.enums.SubscriptionDirection;
import io.reactivex.disposables.Disposable;

View File

@ -33,9 +33,9 @@
android:name=".ui.settings.SettingsActivity"
android:label="@string/title_activity_settings" />
<activity
android:name=".ui.login.LoginActivity"
android:name=".ui.account.LoginActivity"
android:label="@string/title_activity_login" />
<service android:name=".service.XmppConnectionService" />
<service android:name=".service.MercuryConnectionService" />
</application>
</manifest>

View File

@ -0,0 +1,52 @@
package org.mercury_im.messenger;
import android.app.Activity;
import org.mercury_im.messenger.util.AbstractActivityLifecycleCallbacks;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Keep track of activities in "started" state.
* This will come in handy for things like XMPPs CSI.
*
* @see <a href="https://medium.com/@iamsadesh/android-how-to-detect-when-app-goes-background-foreground-fd5a4d331f8a>
* How to detect when app goes foreground/background</a>
*/
public class ClientStateHandler extends AbstractActivityLifecycleCallbacks {
private AtomicInteger activityReferences = new AtomicInteger(0);
private AtomicBoolean isActivityChangingConfiguration = new AtomicBoolean(false);
private final List<ClientStateListener> listeners = new ArrayList<>();
@Override
public void onActivityStarted(Activity activity) {
if (activityReferences.incrementAndGet() == 1 && !isActivityChangingConfiguration.get()) {
for (ClientStateListener listener : listeners) {
listener.onClientInForeground();
}
}
}
@Override
public void onActivityStopped(Activity activity) {
isActivityChangingConfiguration.set(activity.isChangingConfigurations());
if (activityReferences.decrementAndGet() == 0 && !isActivityChangingConfiguration.get()) {
for (ClientStateListener listener : listeners) {
listener.onClientInBackground();
}
}
}
public void addClientStateListener(ClientStateListener listener) {
this.listeners.add(listener);
}
public void removeClientStateListener(ClientStateListener listener) {
this.listeners.remove(listener);
}
}

View File

@ -1,49 +1,40 @@
package org.mercury_im.messenger;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import org.mercury_im.messenger.core.centers.ConnectionCenter;
import org.mercury_im.messenger.core.connection.MercuryConfiguration;
import org.mercury_im.messenger.persistence.util.ChatAndPossiblyContact;
import org.mercury_im.messenger.core.util.ContactNameUtil;
import org.mercury_im.messenger.data.repository.AccountRepository;
import org.mercury_im.messenger.di.component.AppComponent;
import org.mercury_im.messenger.di.component.DaggerAppComponent;
import org.mercury_im.messenger.di.module.AppModule;
import org.mercury_im.messenger.service.XmppConnectionService;
import org.mercury_im.messenger.util.AbstractActivityLifecycleCallbacks;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.service.MercuryConnectionService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.List;
import javax.inject.Inject;
public class MercuryImApplication extends Application implements org.mercury_im.messenger.core.NotificationManager {
import io.reactivex.disposables.CompositeDisposable;
public static final String TAG = "Mercury-IM";
public class MercuryImApplication extends Application {
static {
// Initialize Smack etc.
new MercuryConfiguration();
// new MercuryConfiguration();
}
private static MercuryImApplication INSTANCE;
AppComponent appComponent;
// Keep track of activities in "started" state.
// This will come in handy for CSI
// see https://medium.com/@iamsadesh/android-how-to-detect-when-app-goes-background-foreground-fd5a4d331f8a
private AtomicInteger activityReferences = new AtomicInteger(0);
private AtomicBoolean isActivityChangingConfiguration = new AtomicBoolean(false);
private AppComponent appComponent;
private ClientStateHandler clientStateHandler = new ClientStateHandler();
private final CompositeDisposable disposable = new CompositeDisposable();
@Inject
ConnectionCenter connectionCenter;
AccountRepository accountRepository;
@Inject
Messenger messenger;
public static MercuryImApplication getApplication() {
return INSTANCE;
@ -52,29 +43,25 @@ public class MercuryImApplication extends Application implements org.mercury_im.
@Override
public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(lifecycleCallbacks);
INSTANCE = this;
appComponent = createAppComponent();
appComponent.inject(this);
initializeNotificationChannels(this);
setupClientStateIndication();
Intent serviceIntent = new Intent(getApplicationContext(), XmppConnectionService.class);
Notifications.initializeNotificationChannels(this);
serviceIntent.setAction(XmppConnectionService.ACTION_START);
if (Build.VERSION.SDK_INT < 26) {
startService(serviceIntent);
} else {
startForegroundService(serviceIntent);
}
subscribeForegroundServiceToActiveAccounts();
}
private void setupClientStateIndication() {
clientStateHandler.addClientStateListener(messenger);
registerActivityLifecycleCallbacks(clientStateHandler);
}
/**
* Create the Dependency Injection graph.
* For testing, overwrite this method with custom modules.
*/
public AppComponent createAppComponent() {
AppComponent appComponent = DaggerAppComponent.builder()
@ -86,65 +73,45 @@ public class MercuryImApplication extends Application implements org.mercury_im.
return appComponent;
}
public void initializeNotificationChannels(Context context) {
// Only necessary on Android O and upwards.
if (Build.VERSION.SDK_INT < 26) {
return;
}
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
// Foreground notification channel
String fName = getResources().getString(R.string.channel_name_foreground);
String fDescription = getResources().getString(R.string.channel_description_foreground);
@SuppressLint("WrongConstant")
NotificationChannel foreground = new NotificationChannel(Notifications.NOTIFICATION_CHANNEL__FOREGROUND_SERVICE,
fName, NotificationManager.IMPORTANCE_MIN);
foreground.setDescription(fDescription);
foreground.setShowBadge(false);
notificationManager.createNotificationChannel(foreground);
// Incoming Messages notification channel
String mName = getResources().getString(R.string.channel_name_message);
String mDescription = getResources().getString(R.string.channel_description_message);
@SuppressLint("WrongConstant")
NotificationChannel messages = new NotificationChannel(Notifications.NOTIFICATION_CHANNEL__NEW_MESSAGE,
mName, NotificationManager.IMPORTANCE_DEFAULT);
messages.setDescription(mDescription);
notificationManager.createNotificationChannel(messages);
}
@Override
public int chatMessageReceived(ChatAndPossiblyContact chatAndPossiblyContact, String body) {
return Notifications.chatMessageReceived(this,
chatAndPossiblyContact.getChat(),
ContactNameUtil.displayableNameFrom(chatAndPossiblyContact.getContact()), body);
}
public AppComponent getAppComponent() {
return appComponent;
}
private final AbstractActivityLifecycleCallbacks lifecycleCallbacks = new AbstractActivityLifecycleCallbacks() {
private void subscribeForegroundServiceToActiveAccounts() {
disposable.add(accountRepository.observeAllAccounts()
.map(this::listContainsActiveAccount)
.distinctUntilChanged()
.subscribe(foregroundServiceNeeded -> {
if (foregroundServiceNeeded) {
startForegroundService();
} else {
stopForegroundService();
}
}));
}
@Override
public void onActivityStarted(Activity activity) {
if (activityReferences.incrementAndGet() == 1 && !isActivityChangingConfiguration.get()) {
// App enters foreground
connectionCenter.clientStateActive();
private boolean listContainsActiveAccount(List<Account> accounts) {
for (Account account : accounts) {
if (account.isEnabled()) {
return true;
}
}
return false;
}
@Override
public void onActivityStopped(Activity activity) {
isActivityChangingConfiguration.set(activity.isChangingConfigurations());
if (activityReferences.decrementAndGet() == 0 && !isActivityChangingConfiguration.get()) {
// App enters background
connectionCenter.clientStateInactive();
}
private void startForegroundService() {
Intent startIntent = new Intent(getApplicationContext(), MercuryConnectionService.class);
startIntent.setAction(MercuryConnectionService.ACTION_START);
if (Build.VERSION.SDK_INT < 26) {
startService(startIntent);
} else {
startForegroundService(startIntent);
}
};
}
private void stopForegroundService() {
Intent stopIntent = new Intent(getApplicationContext(), MercuryConnectionService.class);
stopIntent.setAction(MercuryConnectionService.ACTION_STOP);
startService(stopIntent);
}
}

View File

@ -1,15 +1,21 @@
package org.mercury_im.messenger;
import android.annotation.TargetApi;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import org.mercury_im.messenger.persistence.entity.ChatModel;
import org.mercury_im.messenger.entity.chat.DirectChat;
import org.mercury_im.messenger.ui.chat.ChatActivity;
import java.util.UUID;
public class Notifications {
public static final String NOTIFICATION_CHANNEL__FOREGROUND_SERVICE = "foreground_service";
@ -18,15 +24,51 @@ public class Notifications {
public static final String NOTIFICATION_CHANNEL__DEBUG = "debug";
// Notification IDs
public static final int FOREGROUND_SERVICE_ID = 1; // must not be 0
public static final int FOREGROUND_SERVICE_ID = 1;
public static int chatMessageReceived(Context context, ChatModel chat, String contactName, String body) {
public static void initializeNotificationChannels(Context context) {
if (Build.VERSION.SDK_INT < 26) {
// No not call below code on lower API levels
return;
}
createForegroundNotificationChannel(context);
createMessageNotificationChannel(context);
}
@TargetApi(26)
private static void createForegroundNotificationChannel(Context context) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
String channelName = context.getResources().getString(R.string.channel_name_foreground);
String channelDescription = context.getResources().getString(R.string.channel_description_foreground);
NotificationChannel channel = new NotificationChannel(
NOTIFICATION_CHANNEL__FOREGROUND_SERVICE, channelName, NotificationManager.IMPORTANCE_MIN);
channel.setDescription(channelDescription);
channel.setShowBadge(false);
notificationManager.createNotificationChannel(channel);
}
@TargetApi(26)
private static void createMessageNotificationChannel(Context context) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
String channelName = context.getResources().getString(R.string.channel_name_message);
String channelDescription = context.getResources().getString(R.string.channel_description_message);
NotificationChannel channel = new NotificationChannel(
NOTIFICATION_CHANNEL__NEW_MESSAGE, channelName, NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription(channelDescription);
notificationManager.createNotificationChannel(channel);
}
public static int directChatMessageReceived(Context context, DirectChat chat, String contactName, String body) {
NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(context);
int id = (int) chat.getId();
UUID id = chat.getId();
Intent tapAction = new Intent(context, ChatActivity.class);
tapAction.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
tapAction.putExtra(ChatActivity.EXTRA_JID, chat.getPeer().getJid().toString());
tapAction.putExtra(ChatActivity.EXTRA_JID, chat.getPeer().getAddress());
tapAction.putExtra(ChatActivity.EXTRA_ACCOUNT, chat.getPeer().getAccount().getId());
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, tapAction, 0);
@ -38,9 +80,9 @@ public class Notifications {
builder.setContentIntent(pendingIntent);
builder.setAutoCancel(true);
notificationManagerCompat.notify(id, builder.build());
notificationManagerCompat.notify(id.hashCode(), builder.build());
return id;
return id.hashCode();
}
}

View File

@ -1,86 +0,0 @@
package org.mercury_im.messenger;
import android.os.Parcel;
import android.os.Parcelable;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jxmpp.stringprep.XmppStringprepException;
public class ParcelableXMPPTCPConnectionConfiguration implements Parcelable {
private final String username;
private final String password;
private final String xmppDomain;
private final String resourcePart;
private final String host;
private final int port;
private XMPPTCPConnectionConfiguration configuration;
public static final Creator<ParcelableXMPPTCPConnectionConfiguration> CREATOR = new Creator<ParcelableXMPPTCPConnectionConfiguration>() {
@Override
public ParcelableXMPPTCPConnectionConfiguration createFromParcel(Parcel in) {
return new ParcelableXMPPTCPConnectionConfiguration(in);
}
@Override
public ParcelableXMPPTCPConnectionConfiguration[] newArray(int size) {
return new ParcelableXMPPTCPConnectionConfiguration[size];
}
};
public ParcelableXMPPTCPConnectionConfiguration(String username,
String password,
String xmppDomain,
String resourcePart,
String host,
int port) {
this.username = username;
this.password = password;
this.xmppDomain = xmppDomain;
this.resourcePart = resourcePart;
this.host = host;
this.port = port;
}
private ParcelableXMPPTCPConnectionConfiguration(Parcel in) {
this(in.readString(), // username
in.readString(), // password
in.readString(), // xmppDomain
in.readString(), // resourcePart
in.readString(), // host
in.readInt()); // port
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(username);
parcel.writeString(password);
parcel.writeString(xmppDomain);
parcel.writeString(resourcePart);
parcel.writeString(host);
parcel.writeInt(port);
}
public XMPPTCPConnectionConfiguration getConfiguration() throws XmppStringprepException {
if (configuration != null) {
return configuration;
}
XMPPTCPConnectionConfiguration.Builder builder = XMPPTCPConnectionConfiguration.builder();
builder.setUsernameAndPassword(username, password);
if (xmppDomain != null) builder.setXmppDomain(xmppDomain);
if (resourcePart != null) builder.setResource(resourcePart);
if (host != null) builder.setHost(host);
if (port != -1) builder.setPort(port);
configuration = builder.build();
return configuration;
}
}

View File

@ -1,20 +1,20 @@
package org.mercury_im.messenger.di.component;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.core.di.XmppComponent;
import org.mercury_im.messenger.core.stores.PlainMessageStore;
import org.mercury_im.messenger.di.module.AppModule;
import org.mercury_im.messenger.data.di.RepositoryModule;
import org.mercury_im.messenger.di.module.AndroidPersistenceModule;
import org.mercury_im.messenger.service.XmppConnectionService;
import org.mercury_im.messenger.di.module.AppModule;
import org.mercury_im.messenger.service.MercuryConnectionService;
import org.mercury_im.messenger.store.MercuryEntityCapsStore;
import org.mercury_im.messenger.ui.MainActivity;
import org.mercury_im.messenger.ui.chat.ChatActivity;
import org.mercury_im.messenger.ui.chat.ChatInputFragment;
import org.mercury_im.messenger.ui.chat.ChatInputViewModel;
import org.mercury_im.messenger.ui.chat.ChatViewModel;
import org.mercury_im.messenger.ui.chatlist.ChatListViewModel;
import org.mercury_im.messenger.ui.login.AccountsViewModel;
import org.mercury_im.messenger.ui.login.LoginActivity;
import org.mercury_im.messenger.ui.login.LoginViewModel;
import org.mercury_im.messenger.ui.account.AccountsViewModel;
import org.mercury_im.messenger.ui.account.LoginActivity;
import org.mercury_im.messenger.ui.account.LoginViewModel;
import org.mercury_im.messenger.ui.roster.contacts.ContactListViewModel;
import javax.inject.Singleton;
@ -29,7 +29,8 @@ import dagger.Component;
@Component(
modules = {
AppModule.class,
AndroidPersistenceModule.class
AndroidPersistenceModule.class,
RepositoryModule.class
})
public interface AppComponent {
@ -66,11 +67,8 @@ public interface AppComponent {
// Services
void inject(XmppConnectionService service);
void inject(MercuryConnectionService service);
// Connectors
void inject(PlainMessageStore messageHandler);
void inject(MercuryEntityCapsStore store);
}

View File

@ -2,9 +2,9 @@ package org.mercury_im.messenger.di.module;
import android.app.Application;
import org.mercury_im.messenger.data.model.Models;
import org.mercury_im.messenger.util.ThreadUtils;
import org.mercury_im.messenger.BuildConfig;
import org.mercury_im.messenger.persistence.entity.Models;
import org.mercury_im.messenger.thread_utils.ThreadUtils;
import javax.inject.Named;
import javax.inject.Singleton;
@ -29,16 +29,12 @@ public class AndroidPersistenceModule {
@Singleton
static ReactiveEntityStore<Persistable> provideDatabase(Application application) {
// override onUpgrade to handle migrating to a new version
DatabaseSource source = new DatabaseSource(application, Models.DEFAULT,
"mercury_req_db", 1);
DatabaseSource source = new DatabaseSource(application, Models.DEFAULT, "mercury_req_db", 1);
if (BuildConfig.DEBUG) {
// use this in development mode to drop and recreate the tables on every upgrade
source.setTableCreationMode(TableCreationMode.DROP_CREATE);
}
Configuration configuration = source.getConfiguration();
ReactiveEntityStore<Persistable> dataStore = ReactiveSupport.toReactiveStore(
new EntityDataStore<>(configuration));
return dataStore;
return ReactiveSupport.toReactiveStore(new EntityDataStore<>(configuration));
}
@Provides

View File

@ -2,26 +2,22 @@ package org.mercury_im.messenger.di.module;
import android.app.Application;
import dagger.Module;
import dagger.Provides;
import org.jivesoftware.smackx.ping.android.ServerPingWithAlarmManager;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.core.NotificationManager;
import org.mercury_im.messenger.core.di.CenterModule;
import org.mercury_im.messenger.persistence.di.RequeryModule;
import javax.inject.Singleton;
@Module(includes = {
CenterModule.class,
RequeryModule.class
})
import dagger.Module;
import dagger.Provides;
@Module
public class AppModule {
private MercuryImApplication mApplication;
public AppModule(MercuryImApplication application) {
this.mApplication = application;
ServerPingWithAlarmManager.onCreate(application);
}
@Provides
@ -29,10 +25,4 @@ public class AppModule {
Application provideApplication() {
return mApplication;
}
@Provides
@Singleton
NotificationManager providerNotificationManager() {
return mApplication;
}
}

View File

@ -6,7 +6,6 @@ import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
@ -22,36 +21,16 @@ import org.mercury_im.messenger.ui.MainActivity;
* Started, Bound Service, which is responsible for managing {@link XMPPConnection XMPPConnections}
* affiliated with registered accounts.
*/
public class XmppConnectionService extends Service {
private static final String TAG = MercuryImApplication.TAG;
public class MercuryConnectionService extends Service {
private static final String APP = "org.mercury-im.messenger";
private static final String SERVICE = APP + ".XmppConnectionService";
private static final String SERVICE = APP + ".MercuryConnectionService";
private static final String ACTION = SERVICE + ".ACTION";
private static final String EVENT = SERVICE + ".EVENT";
private static final String EXTRA = SERVICE + ".EXTRA";
private static final String STATUS = SERVICE + ".STATUS";
// ACTIONS
public static final String ACTION_START = ACTION + ".START";
public static final String ACTION_STOP = ACTION + ".STOP";
public static final String ACTION_CONNECT = ACTION + ".CONNECT";
public static final String ACTION_DISCONNECT = ACTION + ".DISCONNECT";
public static final String ACTION_PING = ACTION + ".PING";
// EVENTS
public static final String EVENT_INCOMING_MESSAGE = EVENT + ".INCOMING_MESSAGE";
public static final String EVENT_OUTGOING_MESSAGE = EVENT + ".OUTGOING_MESSAGE";
// EXTRAS
public static final String EXTRA_CONFIGURATION = EXTRA + ".CONFIGURATION";
public static final String EXTRA_ACCOUNT_ID = EXTRA + ".ACCOUNT_ID";
// STATUSES
public static final String STATUS_SUCCESS = STATUS + ".SUCCESS";
public static final String STATUS_FAILURE = STATUS + ".FAILURE";
@NonNull
@ -63,27 +42,30 @@ public class XmppConnectionService extends Service {
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate()");
MercuryImApplication.getApplication().getAppComponent().inject(this);
beginLifecycleOfPingManager();
}
// Begin life cycle of Ping Manager.
// The Manager will automatically detect newly created connections and ping the server
// every half hour if necessary.
/**
* PingManager will ensure the XMPP connection is kept alive.
*/
private void beginLifecycleOfPingManager() {
ServerPingWithAlarmManager.onCreate(this);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy()");
// End life cycle of Ping Manager.
endLifecycleOfPingManager();
}
private void endLifecycleOfPingManager() {
ServerPingWithAlarmManager.onDestroy();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand(" + intent + ")");
if (intent == null) {
startAndDisplayForegroundNotification();
} else {
@ -106,19 +88,16 @@ public class XmppConnectionService extends Service {
return START_STICKY_COMPATIBILITY;
}
public void startAndDisplayForegroundNotification() {
Log.d(TAG, "startAndDisplayForegroundNotification()");
Notification notification = getForegroundNotification(getApplicationContext());
private void startAndDisplayForegroundNotification() {
Notification notification = buildForegroundNotification(getApplicationContext());
startForeground(Notifications.FOREGROUND_SERVICE_ID, notification);
}
static Notification getForegroundNotification(Context context) {
private static Notification buildForegroundNotification(Context context) {
Intent startMainActivityIntent = new Intent(context, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0,
startMainActivityIntent, 0);
return new NotificationCompat.Builder(context, Notifications.NOTIFICATION_CHANNEL__FOREGROUND_SERVICE)
.setContentTitle("Mercury")
.setSmallIcon(R.drawable.ic_send_black_24dp)
.setContentIntent(pendingIntent)
.build();
@ -126,13 +105,13 @@ public class XmppConnectionService extends Service {
public class Binder extends android.os.Binder {
private final XmppConnectionService service;
private final MercuryConnectionService service;
public Binder(XmppConnectionService service) {
public Binder(MercuryConnectionService service) {
this.service = service;
}
public XmppConnectionService getService() {
public MercuryConnectionService getService() {
return service;
}
}

View File

@ -15,10 +15,10 @@ import com.google.android.material.navigation.NavigationView;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.persistence.entity.AccountModel;
import org.mercury_im.messenger.persistence.repository.ChatRepository;
import org.mercury_im.messenger.data.repository.AccountRepository;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.ui.chatlist.ChatListFragment;
import org.mercury_im.messenger.ui.login.AccountsFragment;
import org.mercury_im.messenger.ui.account.AccountsFragment;
import org.mercury_im.messenger.ui.roster.RosterFragment;
import org.mercury_im.messenger.ui.settings.SettingsActivity;
@ -31,9 +31,6 @@ public class MainActivity extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener,
AccountsFragment.OnAccountListItemClickListener {
@Inject
ChatRepository chatRepository;
@BindView(R.id.toolbar)
Toolbar toolbar;
@ -44,6 +41,9 @@ public class MainActivity extends AppCompatActivity
private RosterFragment rosterFragment = new RosterFragment();
private AccountsFragment accountsFragment = new AccountsFragment();
@Inject
AccountRepository accountRepository;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -98,12 +98,12 @@ public class MainActivity extends AppCompatActivity
}
@Override
public void onAccountListItemClick(AccountModel item) {
public void onAccountListItemClick(Account item) {
}
@Override
public void onAccountListItemLongClick(AccountModel item) {
public void onAccountListItemLongClick(Account item) {
accountRepository.deleteAccount(item).subscribe();
}
}

View File

@ -1,9 +1,8 @@
package org.mercury_im.messenger.ui.login;
package org.mercury_im.messenger.ui.account;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -15,9 +14,8 @@ import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.persistence.entity.AccountModel;
import org.mercury_im.messenger.entity.Account;
import butterknife.BindView;
import butterknife.ButterKnife;
@ -42,45 +40,42 @@ public class AccountsFragment extends Fragment {
@BindView(R.id.fab)
ExtendedFloatingActionButton fab;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public AccountsFragment() {
}
// TODO: Customize parameter initialization
public static AccountsFragment newInstance() {
AccountsFragment fragment = new AccountsFragment();
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(AccountsViewModel.class);
this.adapter = new AccountsRecyclerViewAdapter(viewModel, accountClickListener);
}
@Override
public void onResume() {
super.onResume();
observeViewModel();
}
private void observeViewModel() {
viewModel.getConnections().observe(this, adapter::setValues);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d(MercuryImApplication.TAG, "AccountsFragment.onCreateView");
View view = inflater.inflate(R.layout.fragment_account_list, container, false);
ButterKnife.bind(this, view);
viewModel = new ViewModelProvider(this).get(AccountsViewModel.class);
// Set the adapter
Context context = view.getContext();
fab.setOnClickListener(v -> startActivity(new Intent(context, LoginActivity.class)));
recyclerView.setLayoutManager(new LinearLayoutManager(context));
this.adapter = new AccountsRecyclerViewAdapter(viewModel, accountClickListener);
viewModel.getAccounts().observe(this, roomAccountModels -> adapter.setValues(roomAccountModels));
recyclerView.setAdapter(adapter);
return view;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
@ -109,8 +104,8 @@ public class AccountsFragment extends Fragment {
* >Communicating with Other Fragments</a> for more information.
*/
public interface OnAccountListItemClickListener {
void onAccountListItemClick(AccountModel item);
void onAccountListItemClick(Account item);
void onAccountListItemLongClick(AccountModel item);
void onAccountListItemLongClick(Account item);
}
}

View File

@ -0,0 +1,129 @@
package org.mercury_im.messenger.ui.account;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.Switch;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import org.jetbrains.annotations.NotNull;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.ui.avatar.AvatarDrawable;
import org.mercury_im.messenger.ui.account.AccountsFragment.OnAccountListItemClickListener;
import org.mercury_im.messenger.util.AbstractDiffCallback;
import org.mercury_im.messenger.xmpp.MercuryConnection;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers;
public class AccountsRecyclerViewAdapter extends RecyclerView.Adapter<AccountsRecyclerViewAdapter.ViewHolder> {
private final List<MercuryConnection> connections = new ArrayList<>();
private final OnAccountListItemClickListener onAccountClickListener;
private final AccountsViewModel viewModel;
public AccountsRecyclerViewAdapter(AccountsViewModel viewModel, OnAccountListItemClickListener listener) {
onAccountClickListener = listener;
this.viewModel = viewModel;
}
@NotNull
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item_account, parent, false);
return new ViewHolder(view);
}
public void setValues(List<MercuryConnection> values) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
new AccountsDiffCallback(values, connections), true);
connections.clear();
connections.addAll(values);
diffResult.dispatchUpdatesTo(this);
}
@Override
public int getItemCount() {
return connections.size();
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
MercuryConnection connection = connections.get(position);
Account account = connection.getAccount();
holder.account = account;
holder.jid.setText(account.getAddress());
holder.avatar.setImageDrawable(new AvatarDrawable(account.getAddress(), account.getAddress()));
holder.enabled.setChecked(account.isEnabled());
holder.enabled.setOnCheckedChangeListener((compoundButton, checked) ->
viewModel.setAccountEnabled(account, checked));
viewModel.getDisposable().add(connection.getState()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(state -> holder.status.setText(state.toString())));
setClickListenersOnViewHolder(holder);
}
private void setClickListenersOnViewHolder(ViewHolder holder) {
holder.mView.setOnClickListener(v -> {
if (null != onAccountClickListener) {
onAccountClickListener.onAccountListItemClick(holder.account);
}
});
holder.mView.setOnLongClickListener(v -> {
if (null != onAccountClickListener) {
onAccountClickListener.onAccountListItemLongClick(holder.account);
}
return true;
});
}
class ViewHolder extends RecyclerView.ViewHolder {
private Account account;
final View mView;
final ImageView avatar;
final TextView jid;
final Switch enabled;
final TextView status;
public ViewHolder(View view) {
super(view);
mView = view;
avatar = view.findViewById(R.id.avatar_account);
jid = view.findViewById(R.id.text_account_jid);
enabled = view.findViewById(R.id.switch_account_enabled);
status = view.findViewById(R.id.text_account_status);
}
}
public class AccountsDiffCallback extends AbstractDiffCallback<MercuryConnection> {
public AccountsDiffCallback(List<MercuryConnection> newItems, List<MercuryConnection> oldItems) {
super(newItems, oldItems);
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldItems.get(oldItemPosition).getAccount().getId() == newItems.get(newItemPosition).getAccount().getId();
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
Account oldM = oldItems.get(oldItemPosition).getAccount();
Account newM = newItems.get(newItemPosition).getAccount();
return oldM.equals(newM);
}
}
}

View File

@ -0,0 +1,64 @@
package org.mercury_im.messenger.ui.account;
import android.app.Application;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import org.mercury_im.messenger.MercuryImApplication;
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.xmpp.MercuryConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import io.reactivex.disposables.CompositeDisposable;
import lombok.Getter;
public class AccountsViewModel extends AndroidViewModel {
@Inject
AccountRepository repository;
@Inject
Messenger messenger;
private final MutableLiveData<List<MercuryConnection>> connections = new MutableLiveData<>();
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
@Inject
public AccountsViewModel(Application application) {
super(application);
MercuryImApplication.getApplication().getAppComponent().inject(this);
compositeDisposable.add(messenger.getConnectionManager()
.observeConnections()
.map(Map::values)
.subscribe(conns -> connections.postValue(new ArrayList<>(conns))));
}
@Override
protected void onCleared() {
super.onCleared();
compositeDisposable.clear();
}
public CompositeDisposable getDisposable() {
return compositeDisposable;
}
public LiveData<List<MercuryConnection>> getConnections() {
return connections;
}
public void setAccountEnabled(Account accountModel, boolean enabled) {
accountModel.setEnabled(enabled);
repository.upsertAccount(accountModel)
.subscribe();
}
}

View File

@ -0,0 +1,158 @@
package org.mercury_im.messenger.ui.account;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.textfield.TextInputEditText;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.account.error.PasswordError;
import org.mercury_im.messenger.account.error.UsernameError;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.util.Optional;
import org.mercury_im.messenger.util.TextChangedListener;
import butterknife.BindView;
import butterknife.ButterKnife;
/**
* A login screen that offers login via email/password.
*/
public class LoginActivity extends AppCompatActivity implements TextView.OnEditorActionListener {
@BindView(R.id.username)
TextInputEditText addressView;
@BindView(R.id.password)
TextInputEditText passwordView;
@BindView(R.id.sign_in_button)
Button loginButton;
private LoginViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
ButterKnife.bind(this);
MercuryImApplication.getApplication().getAppComponent().inject(this);
viewModel = new ViewModelProvider(this).get(LoginViewModel.class);
observeViewModel();
setupTextInputListeners();
loginButton.setOnClickListener(view -> viewModel.login());
}
private void observeViewModel() {
observeUsernameError();
observePasswordError();
finishOnceLoginWasSuccessful();
enableLoginButtonOncePossible();
}
private void observeUsernameError() {
viewModel.getUsernameError().observe(this, usernameError -> {
addressView.setError(usernameError.isError() ? usernameError.getErrorMessage() : null);
});
}
private void observePasswordError() {
viewModel.getPasswordError().observe(this, passwordError -> {
passwordView.setError(passwordError.isError() ? passwordError.getErrorMessage() : null);
});
}
private void finishOnceLoginWasSuccessful() {
viewModel.isLoginSuccessful().observe(this, successful -> {
if (Boolean.TRUE.equals(successful)) {
finish();
}
});
}
private void enableLoginButtonOncePossible() {
viewModel.isLoginEnabled().observe(this, isEnabled -> {
loginButton.setEnabled(isEnabled);
});
}
private Optional<String> getUsernameError(UsernameError usernameError) {
switch (usernameError) {
case none:
return new Optional<>(null);
case emptyUsername:
return new Optional<>(getResources().getString(R.string.error_field_required));
case invalidUsername:
return new Optional<>(getResources().getString(R.string.error_invalid_username));
case unknownUsername:
return new Optional<>("Unknown Username!");
default:
throw new AssertionError("Unknown UsernameError enum value.");
}
}
private Optional<String> getPasswordError(PasswordError passwordError) {
switch (passwordError) {
case none:
return new Optional<>(null);
case emptyPassword:
return new Optional<>(getResources().getString(R.string.error_field_required));
case invalidPassword:
return new Optional<>(getResources().getString(R.string.error_invalid_password));
case incorrectPassword:
return new Optional<>(getResources().getString(R.string.error_incorrect_password));
default:
throw new AssertionError("Unknown PasswordError enum value.");
}
}
private void setupTextInputListeners() {
addressView.setOnEditorActionListener(this);
passwordView.setOnEditorActionListener(this);
addressView.addTextChangedListener(new TextChangedListener() {
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
viewModel.setUsername(charSequence.toString());
}
});
passwordView.addTextChangedListener(new TextChangedListener() {
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
viewModel.setPassword(charSequence.toString());
}
});
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
switch (v.getId()) {
case R.id.username:
if (actionId == EditorInfo.IME_ACTION_NEXT) {
passwordView.requestFocus();
return true;
}
break;
case R.id.password:
if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) {
//viewModel.login();
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,155 @@
package org.mercury_im.messenger.ui.account;
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 org.jivesoftware.smack.sasl.SASLErrorException;
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.Messenger;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.entity.IAccount;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
public class LoginViewModel extends AndroidViewModel {
private MutableLiveData<Error> usernameError = new MutableLiveData<>(new Error());
private MutableLiveData<Error> passwordError = new MutableLiveData<>(new Error());
private MutableLiveData<Boolean> loginButtonEnabled = new MutableLiveData<>(false);
private MutableLiveData<Boolean> loginCompleted = new MutableLiveData<>(false);
private final CompositeDisposable disposable = new CompositeDisposable();
private EntityBareJid username;
private String password;
@Inject
Messenger messenger;
public LoginViewModel(@NonNull Application application) {
super(application);
((MercuryImApplication) application).getAppComponent().inject(this);
}
public void setUsername(String username) {
if (username == null || username.isEmpty()) {
this.username = null;
usernameError.setValue(new Error(getApplication().getResources().getString(R.string.error_field_required)));
} else {
try {
this.username = JidCreate.entityBareFrom(username);
} catch (XmppStringprepException e) {
this.username = null;
usernameError.setValue(new Error(getApplication().getResources().getString(R.string.error_invalid_username)));
}
}
updateLoginButtonState();
}
public void setPassword(String password) {
if (password == null || password.isEmpty()) {
this.password = null;
passwordError.setValue(new Error(getApplication().getResources().getString(R.string.error_field_required)));
} else {
this.password = password;
}
updateLoginButtonState();
}
private void updateLoginButtonState() {
loginButtonEnabled.setValue(username != null && !(password == null || password.isEmpty()));
}
public synchronized void login() {
Boolean loginEnabled = loginButtonEnabled.getValue();
if (loginEnabled != null && !loginEnabled) {
// Prevent race condition where account would be logged in twice
return;
}
loginButtonEnabled.setValue(false);
Account account = new IAccount();
account.setAddress(username.asUnescapedString());
account.setPassword(password);
disposable.add(messenger.addAccount()
.execute(account)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.doOnComplete(this::signalLoginSuccessful)
.doOnError(this::signalLoginError)
.subscribe());
}
private void signalLoginSuccessful() {
Logger.getAnonymousLogger().log(Level.INFO, "Signal Login Successful");
loginCompleted.setValue(true);
}
private void signalLoginError(Throwable error) {
if (error instanceof SASLErrorException) {
passwordError.setValue(new Error(getApplication().getResources().getString(R.string.error_incorrect_password)));
loginButtonEnabled.setValue(true);
} else {
Toast.makeText(getApplication(), "A connection error occurred", Toast.LENGTH_LONG).show();
loginButtonEnabled.setValue(true);
}
}
public LiveData<Error> getPasswordError() {
return passwordError;
}
public LiveData<Error> getUsernameError() {
return usernameError;
}
public LiveData<Boolean> isLoginSuccessful() {
return loginCompleted;
}
public LiveData<Boolean> isLoginEnabled() {
return loginButtonEnabled;
}
@Override
protected void onCleared() {
super.onCleared();
disposable.clear();
}
public static class Error {
private String message;
public Error() {
}
public Error(String errorMessage) {
this.message = errorMessage;
}
public boolean isError() {
return message != null;
}
public String getErrorMessage() {
return message;
}
}
}

View File

@ -1,4 +1,4 @@
/**
* Some Javadoc information about the package.
*/
package org.mercury_im.messenger.ui.login;
package org.mercury_im.messenger.ui.account;

View File

@ -0,0 +1,71 @@
/*
* Copyright 2019 Daniel Gultsch
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mercury_im.messenger.ui.avatar;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import org.mercury_im.messenger.util.ColorUtil;
/**
* Generates a round colored drawable with white initials.
* Code courtesy of Daniel Gultsch's lttrs-android application.
*
* @see <a href="https://github.com/iNPUTmice/lttrs-android">Ltt.rs for Android</a>
*/
public class AvatarDrawable extends ColorDrawable {
private final Paint paint;
private final Paint textPaint;
private String letter;
public AvatarDrawable(String name, String key) {
paint = new Paint();
paint.setColor(key == null ? 0xff757575 : ColorUtil.consistentColor(key));
paint.setAntiAlias(true);
textPaint = new Paint();
textPaint.setColor(Color.WHITE);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setAntiAlias(true);
textPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL));
this.letter = name == null ? null : String.valueOf(Character.toUpperCase(name.charAt(0)));
}
@Override
public void draw(Canvas canvas) {
float midx = getBounds().width() / 2.0f;
float midy = getBounds().height() / 2.0f;
float radius = Math.min(getBounds().width(), getBounds().height()) / 2.0f;
textPaint.setTextSize(radius);
Rect r = new Rect();
canvas.getClipBounds(r);
int cHeight = r.height();
int cWidth = r.width();
canvas.drawCircle(midx, midy, radius, paint);
if (letter == null) {
return;
}
textPaint.setTextAlign(Paint.Align.LEFT);
textPaint.getTextBounds(letter, 0, letter.length(), r);
float x = cWidth / 2f - r.width() / 2f - r.left;
float y = cHeight / 2f + r.height() / 2f - r.bottom;
canvas.drawText(letter, x, y, textPaint);
}
}

View File

@ -11,31 +11,20 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.chat2.ChatManager;
import org.jivesoftware.smack.packet.Message;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.core.centers.ConnectionCenter;
import org.mercury_im.messenger.persistence.repository.ChatRepository;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.UUID;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
public class ChatActivity extends AppCompatActivity
implements ChatInputFragment.OnChatInputActionListener, SearchView.OnQueryTextListener {
@ -43,18 +32,12 @@ public class ChatActivity extends AppCompatActivity
public static final String EXTRA_JID = "JID";
public static final String EXTRA_ACCOUNT = "ACCOUNT";
@Inject
ConnectionCenter connectionCenter;
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.recyclerView)
RecyclerView recyclerView;
@Inject
ChatRepository chatRepository;
private final MessagesRecyclerViewAdapter recyclerViewAdapter = new MessagesRecyclerViewAdapter();
private ChatViewModel chatViewModel;
@ -63,7 +46,7 @@ public class ChatActivity extends AppCompatActivity
private EntityBareJid jid;
private long accountId;
private UUID accountId;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -91,7 +74,7 @@ public class ChatActivity extends AppCompatActivity
jid = JidCreate.entityBareFromOrThrowUnchecked(jidString);
// JID will never change, so just set it once
getSupportActionBar().setSubtitle(jid.asUnescapedString());
accountId = savedInstanceState.getLong(EXTRA_ACCOUNT);
accountId = UUID.fromString(savedInstanceState.getString(EXTRA_ACCOUNT));
chatViewModel = new ViewModelProvider(this).get(ChatViewModel.class);
chatViewModel.init(accountId, jid);
@ -163,7 +146,7 @@ public class ChatActivity extends AppCompatActivity
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
outState.putString(EXTRA_JID, jid.toString());
outState.putLong(EXTRA_ACCOUNT, accountId);
outState.putString(EXTRA_ACCOUNT, accountId.toString());
super.onSaveInstanceState(outState);
}
@ -189,6 +172,7 @@ public class ChatActivity extends AppCompatActivity
return;
}
/*
// TODO: Improve by using rx
new Thread() {
@Override
@ -203,6 +187,8 @@ public class ChatActivity extends AppCompatActivity
}
}
}.start();
*/
}
@Override

View File

@ -1,6 +1,5 @@
package org.mercury_im.messenger.ui.chat;
import static org.mercury_im.messenger.MercuryImApplication.TAG;
import android.content.Context;
import android.os.Bundle;
@ -14,7 +13,7 @@ import android.widget.ImageButton;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
import butterknife.BindView;
import butterknife.ButterKnife;
@ -48,7 +47,6 @@ public class ChatInputFragment extends Fragment implements View.OnClickListener
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
Log.d(TAG, "onCreateView");
View view = inflater.inflate(R.layout.view_compose, container, false);
ButterKnife.bind(this, view);
return view;
@ -57,7 +55,7 @@ public class ChatInputFragment extends Fragment implements View.OnClickListener
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mViewModel = ViewModelProviders.of(this).get(ChatInputViewModel.class);
mViewModel = new ViewModelProvider(this).get(ChatInputViewModel.class);
observeViewModel(mViewModel);
}
@ -97,7 +95,6 @@ public class ChatInputFragment extends Fragment implements View.OnClickListener
@Override
@OnClick({R.id.btn_send, R.id.btn_media, R.id.btn_emoji})
public void onClick(View view) {
Log.d(TAG, "onClick!");
switch (view.getId()) {
// Add media
case R.id.btn_media:

View File

@ -6,69 +6,68 @@ import androidx.lifecycle.ViewModel;
import org.jxmpp.jid.EntityBareJid;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.core.centers.ConnectionCenter;
import org.mercury_im.messenger.persistence.entity.ChatModel;
import org.mercury_im.messenger.persistence.entity.ContactModel;
import org.mercury_im.messenger.persistence.entity.EntityModel;
import org.mercury_im.messenger.persistence.entity.MessageModel;
import org.mercury_im.messenger.persistence.repository.ChatRepository;
import org.mercury_im.messenger.persistence.repository.MessageRepository;
import org.mercury_im.messenger.persistence.repository.RosterRepository;
import org.mercury_im.messenger.data.repository.DirectChatRepository;
import org.mercury_im.messenger.data.repository.MessageRepository;
import org.mercury_im.messenger.data.repository.PeerRepository;
import org.mercury_im.messenger.entity.chat.DirectChat;
import org.mercury_im.messenger.entity.contact.Peer;
import org.mercury_im.messenger.entity.message.Message;
import java.util.List;
import java.util.UUID;
import javax.inject.Inject;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
public class ChatViewModel extends ViewModel {
private final CompositeDisposable disposable = new CompositeDisposable();
@Inject
PeerRepository contactRepository;
@Inject
DirectChatRepository chatRepository;
@Inject
MessageRepository messageRepository;
@Inject
RosterRepository rosterRepository;
@Inject
ChatRepository chatRepository;
@Inject
ConnectionCenter connectionCenter;
private MutableLiveData<EntityModel> entity = new MutableLiveData<>();
private MutableLiveData<ContactModel> contact = new MutableLiveData<>();
private MutableLiveData<List<MessageModel>> messages = new MutableLiveData<>();
private MutableLiveData<Peer> contact = new MutableLiveData<>();
private MutableLiveData<List<Message>> messages = new MutableLiveData<>();
private MutableLiveData<String> contactDisplayName = new MutableLiveData<>();
private MutableLiveData<ChatModel> chat = new MutableLiveData<>();
private MutableLiveData<DirectChat> chat = new MutableLiveData<>();
public ChatViewModel() {
super();
MercuryImApplication.getApplication().getAppComponent().inject(this);
}
public void init(long accountId, EntityBareJid jid) {
disposable.add(rosterRepository.getOrCreateEntity(accountId, jid)
.subscribe((Consumer<EntityModel>) this::init));
public void init(UUID accountId, EntityBareJid jid) {
disposable.add(contactRepository.getOrCreatePeer(accountId, jid.toString())
.subscribe((Consumer<Peer>) this::init));
}
public void init(EntityModel entityModel) {
public void init(Peer peer) {
disposable.add(chatRepository.getOrCreateChatWithPeer(peer)
.subscribe((Consumer<DirectChat>) this::init));
}
disposable.add(rosterRepository.getContact(entityModel.getAccount().getId(), entityModel.getJid())
.subscribe(reactiveResult -> {
ContactModel model = reactiveResult.first();
ChatViewModel.this.contact.setValue(model);
contactDisplayName.setValue(model.getRostername());
}));
public void init(DirectChat chat) {
this.chat.setValue(chat);
disposable.add(messageRepository.getAllMessagesOfEntity(entityModel)
.subscribe(reactiveResult -> {
List<MessageModel> messages = reactiveResult.toList();
ChatViewModel.this.messages.setValue(messages);
}));
// Subscribe peer
disposable.add(contactRepository.observePeer(chat.getPeer().getId())
.subscribe(peer -> contactDisplayName.setValue(peer.getItem().getName())));
// Subscribe messages
disposable.add(messageRepository.observeMessages(chat)
.subscribe(ChatViewModel.this.messages::setValue));
}
@Override
@ -77,11 +76,11 @@ public class ChatViewModel extends ViewModel {
disposable.clear();
}
public LiveData<List<MessageModel>> getMessages() {
public LiveData<List<Message>> getMessages() {
return messages;
}
public LiveData<ContactModel> getContact() {
public LiveData<Peer> getContact() {
return contact;
}
@ -90,25 +89,16 @@ public class ChatViewModel extends ViewModel {
}
public void queryTextChanged(String query) {
/*
if (query.isEmpty()) {
disposable.add(messageRepository.getAllMessagesOfChat(accountId, jid)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((Consumer<List<MessageModel>>)
messages -> ChatViewModel.this.messages.setValue(messages)));
}
disposable.add(messageRepository.findMessageByQuery(accountId, jid, query)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((Consumer<List<MessageModel>>) o -> {
messages.setValue(o);
}));
Observable<List<Message>> observable = query.isEmpty() ?
messageRepository.observeMessages(chat.getValue()) :
messageRepository.findMessagesWithBody(chat.getValue(), query);
*/
disposable.add(observable.subscribe(messages ->
ChatViewModel.this.messages.setValue(messages)));
}
public Completable requestMamMessages() {
/*
return Completable.fromAction(() -> {
ChatModel chatModel = ChatViewModel.this.chat.getValue();
if (chatModel == null) {
@ -117,5 +107,7 @@ public class ChatViewModel extends ViewModel {
connectionCenter.requestMamMessagesFor(chatModel);
});
*/
return null;
}
}

View File

@ -1,6 +1,6 @@
package org.mercury_im.messenger.ui.chat;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -10,7 +10,8 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.persistence.entity.MessageModel;
import org.mercury_im.messenger.entity.message.Message;
import org.mercury_im.messenger.entity.message.content.TextPayload;
import org.mercury_im.messenger.ui.util.MessageBackgroundDrawable;
import java.util.ArrayList;
@ -18,14 +19,14 @@ import java.util.List;
public class MessagesRecyclerViewAdapter extends RecyclerView.Adapter<MessagesRecyclerViewAdapter.MessageViewHolder> {
private List<MessageModel> messages = new ArrayList<>();
private SparseArray<Boolean> checkedItems = new SparseArray<>();
private List<Message> messages = new ArrayList<>();
private SparseBooleanArray checkedItems = new SparseBooleanArray();
public MessagesRecyclerViewAdapter() {
}
public void updateMessages(List<MessageModel> messages) {
public void updateMessages(List<Message> messages) {
this.messages.clear();
this.messages.addAll(messages);
notifyDataSetChanged();
@ -47,14 +48,14 @@ public class MessagesRecyclerViewAdapter extends RecyclerView.Adapter<MessagesRe
@Override
public void onBindViewHolder(@NonNull MessageViewHolder holder, int position) {
MessageModel message = messages.get(position);
Message message = messages.get(position);
MessageBackgroundDrawable background = getBackgroundDrawable(position);
if (message.isIncoming()) {
holder.body.setBackgroundResource(background.getIncomingDrawable());
} else {
holder.body.setBackgroundResource(background.getOutgoingDrawable());
holder.body.setBackgroundResource(background.getOutgoingDrawable());
}
holder.body.setText(message.getBody());
holder.body.setText(((TextPayload) message.getMessagePayloads().get(0).getMessageContents().get(0)).getBody());
holder.body.requestLayout();
}
@ -84,9 +85,9 @@ public class MessagesRecyclerViewAdapter extends RecyclerView.Adapter<MessagesRe
}
public MessageBackgroundDrawable getBackgroundDrawable(int pos) {
MessageModel subject = messages.get(pos);
MessageModel predecessor = pos != 0 ? messages.get(pos - 1) : null;
MessageModel successor = pos != messages.size() - 1 ? messages.get(pos + 1) : null;
Message subject = messages.get(pos);
Message predecessor = pos != 0 ? messages.get(pos - 1) : null;
Message successor = pos != messages.size() - 1 ? messages.get(pos + 1) : null;
if (predecessor == null || predecessor.isIncoming() != subject.isIncoming()) {
if (successor == null || successor.isIncoming() != subject.isIncoming()) {

View File

@ -1,8 +1,6 @@
package org.mercury_im.messenger.ui.chatlist;
import static org.mercury_im.messenger.MercuryImApplication.TAG;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
@ -13,16 +11,17 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
import org.mercury_im.messenger.Messenger;
import org.mercury_im.messenger.R;
import butterknife.BindView;
import butterknife.ButterKnife;
import org.mercury_im.messenger.R;
public class ChatListFragment extends Fragment {
private ChatListViewModel viewModel;
@ -34,8 +33,10 @@ public class ChatListFragment extends Fragment {
@BindView(R.id.fab)
ExtendedFloatingActionButton fab;
public ChatListFragment() {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ChatListViewModel.class);
}
@Nullable
@ -53,21 +54,14 @@ public class ChatListFragment extends Fragment {
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
public void onResume() {
super.onResume();
observeViewModel();
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
viewModel = ViewModelProviders.of(getActivity()).get(ChatListViewModel.class);
private void observeViewModel() {
viewModel.getChats().observe(this, chatModels -> {
if (chatModels == null) {
Log.d(TAG, "Displaying null chats");
return;
}
Log.d(TAG, "Displaying " + chatModels.size() + " chats");
Log.d(Messenger.TAG, "Displaying " + chatModels.size() + " chats");
recyclerViewAdapter.setModels(chatModels);
});
}

View File

@ -16,16 +16,20 @@ import androidx.appcompat.view.ActionMode;
import androidx.recyclerview.widget.RecyclerView;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.persistence.entity.ChatModel;
import org.mercury_im.messenger.entity.chat.DirectChat;
import org.mercury_im.messenger.ui.avatar.AvatarDrawable;
import org.mercury_im.messenger.ui.chat.ChatActivity;
import org.mercury_im.messenger.ui.util.AbstractRecyclerViewAdapter;
import org.mercury_im.messenger.util.ColorUtil;
import java.util.logging.Level;
import java.util.logging.Logger;
import butterknife.BindView;
import butterknife.ButterKnife;
public class ChatListRecyclerViewAdapter
extends AbstractRecyclerViewAdapter<ChatModel, ChatListRecyclerViewAdapter.ChatHolder> {
extends AbstractRecyclerViewAdapter<DirectChat, ChatListRecyclerViewAdapter.ChatHolder> {
public ChatListRecyclerViewAdapter() {
super(new ChatMessageDiffCallback(true));
@ -41,16 +45,17 @@ public class ChatListRecyclerViewAdapter
@Override
public void onBindViewHolder(@NonNull ChatHolder holder, int position) {
ChatModel model = getModelAt(position);
holder.nameView.setText(model.getPeer().getJid() != null ?
model.getPeer().getJid() : model.toString());
holder.avatarView.setColorFilter(ColorUtil.consistentColor(model.getPeer().getJid().toString()));
Logger.getAnonymousLogger().log(Level.INFO, "BIND");
DirectChat model = getItemAt(position);
String name = model.getPeer().getDisplayName();
String address = model.getPeer().getAddress();
holder.nameView.setText(name);
holder.avatarView.setImageDrawable(new AvatarDrawable(name, address));
holder.itemView.setOnClickListener(view -> {
Intent intent = new Intent(holder.context, ChatActivity.class);
intent.putExtra(ChatActivity.EXTRA_JID, model.getPeer().getJid().toString());
intent.putExtra(ChatActivity.EXTRA_ACCOUNT, model.getPeer().getAccount().getId());
intent.putExtra(ChatActivity.EXTRA_JID, model.getPeer().getAddress());
intent.putExtra(ChatActivity.EXTRA_ACCOUNT, model.getPeer().getAccount().getId().toString());
holder.context.startActivity(intent);
});
@ -84,20 +89,20 @@ public class ChatListRecyclerViewAdapter
}
}
private static class ChatMessageDiffCallback extends AbstractDiffCallback<ChatModel> {
private static class ChatMessageDiffCallback extends AbstractDiffCallback<DirectChat> {
ChatMessageDiffCallback(boolean detectMoves) {
super(detectMoves);
}
@Override
public boolean areItemsTheSame(ChatModel oldItem, ChatModel newItem) {
public boolean areItemsTheSame(DirectChat oldItem, DirectChat newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(ChatModel oldItem, ChatModel newItem) {
return areItemsTheSame(oldItem, newItem);
public boolean areContentsTheSame(DirectChat oldItem, DirectChat newItem) {
return oldItem.getPeer().getName().equals(newItem.getPeer().getName());
}
}

View File

@ -5,10 +5,10 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.persistence.entity.ChatModel;
import org.mercury_im.messenger.persistence.repository.ChatRepository;
import org.mercury_im.messenger.persistence.repository.MessageRepository;
import org.mercury_im.messenger.data.repository.DirectChatRepository;
import org.mercury_im.messenger.entity.chat.DirectChat;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
@ -18,23 +18,20 @@ import io.reactivex.disposables.CompositeDisposable;
public class ChatListViewModel extends ViewModel {
@Inject
ChatRepository chatRepository;
@Inject
MessageRepository messageRepository;
DirectChatRepository chatRepository;
private CompositeDisposable disposable = new CompositeDisposable();
private final MutableLiveData<List<ChatModel>> chats = new MutableLiveData<>();
private final MutableLiveData<List<DirectChat>> chats = new MutableLiveData<>(new ArrayList<>());
public ChatListViewModel() {
MercuryImApplication.getApplication().getAppComponent().inject(this);
disposable.add(chatRepository.getVisibleChats()
.subscribe(result -> chats.setValue(result.toList())));
disposable.add(chatRepository.observeAllDirectChats()
.subscribe(chats::setValue));
}
public LiveData<List<ChatModel>> getChats() {
public LiveData<List<DirectChat>> getChats() {
return chats;
}

View File

@ -1,117 +0,0 @@
package org.mercury_im.messenger.ui.login;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.Switch;
import android.widget.TextView;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.persistence.entity.AccountModel;
import org.mercury_im.messenger.ui.login.AccountsFragment.OnAccountListItemClickListener;
import org.mercury_im.messenger.util.AbstractDiffCallback;
import org.mercury_im.messenger.util.ColorUtil;
import java.util.ArrayList;
import java.util.List;
public class AccountsRecyclerViewAdapter extends RecyclerView.Adapter<AccountsRecyclerViewAdapter.ViewHolder> {
private final List<AccountModel> mValues;
private final OnAccountListItemClickListener mListener;
private final AccountsViewModel viewModel;
public AccountsRecyclerViewAdapter(AccountsViewModel viewModel, OnAccountListItemClickListener listener) {
mValues = new ArrayList<>();
mListener = listener;
this.viewModel = viewModel;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item_account, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
AccountModel account = mValues.get(position);
holder.jid.setText(account.getJid());
holder.avatar.setColorFilter(ColorUtil.consistentColor(account.getJid().toString()));
holder.enabled.setChecked(account.isEnabled());
holder.accountModel = account;
holder.enabled.setOnCheckedChangeListener((compoundButton, b) -> {
viewModel.toggleAccountEnabled(account);
});
holder.mView.setOnClickListener(v -> {
if (null != mListener) {
// Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected.
mListener.onAccountListItemClick(holder.accountModel);
}
});
holder.mView.setOnLongClickListener(v -> {
if (null != mListener) {
mListener.onAccountListItemLongClick(holder.accountModel);
}
return true;
});
}
public void setValues(List<AccountModel> values) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new AccountsDiffCallback(values, mValues), true);
mValues.clear();
mValues.addAll(values);
diffResult.dispatchUpdatesTo(this);
}
@Override
public int getItemCount() {
Log.d(MercuryImApplication.TAG, "Accounts Item Count: " + mValues.size());
return mValues.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
public AccountModel accountModel;
public final View mView;
public final ImageView avatar;
public final TextView jid;
public final Switch enabled;
public final TextView status;
public ViewHolder(View view) {
super(view);
mView = view;
avatar = view.findViewById(R.id.avatar_account);
jid = view.findViewById(R.id.text_account_jid);
enabled = view.findViewById(R.id.switch_account_enabled);
status = view.findViewById(R.id.text_account_status);
}
}
public class AccountsDiffCallback extends AbstractDiffCallback<AccountModel> {
public AccountsDiffCallback(List<AccountModel> newItems, List<AccountModel> oldItems) {
super(newItems, oldItems);
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldItems.get(oldItemPosition).getId() == newItems.get(newItemPosition).getId();
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
AccountModel oldM = oldItems.get(oldItemPosition);
AccountModel newM = newItems.get(newItemPosition);
return oldM.equals(newM);
}
}
}

View File

@ -1,62 +0,0 @@
package org.mercury_im.messenger.ui.login;
import android.app.Application;
import android.widget.Toast;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.core.centers.ConnectionCenter;
import org.mercury_im.messenger.core.connection.MercuryConnection;
import org.mercury_im.messenger.persistence.entity.AccountModel;
import org.mercury_im.messenger.persistence.repository.AccountRepository;
import java.util.List;
import javax.inject.Inject;
import io.reactivex.disposables.CompositeDisposable;
public class AccountsViewModel extends AndroidViewModel {
@Inject
AccountRepository repository;
@Inject
ConnectionCenter connectionCenter;
private final MutableLiveData<List<AccountModel>> accounts = new MutableLiveData<>();
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
@Inject
public AccountsViewModel(Application application) {
super(application);
MercuryImApplication.getApplication().getAppComponent().inject(this);
compositeDisposable.add(repository.getAll()
.subscribe(accountModels -> accounts.setValue(accountModels.toList())));
}
@Override
protected void onCleared() {
super.onCleared();
compositeDisposable.clear();
}
public LiveData<List<AccountModel>> getAccounts() {
return accounts;
}
public void toggleAccountEnabled(AccountModel accountModel) {
MercuryConnection connection = connectionCenter.getConnection(accountModel);
if (connection == null) {
Toast.makeText(this.getApplication(), "MercuryConnection is null!", Toast.LENGTH_LONG).show();
return;
}
accountModel.setEnabled(!accountModel.isEnabled());
repository.upsert(accountModel)
.subscribe();
}
}

View File

@ -1,21 +0,0 @@
package org.mercury_im.messenger.ui.login;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
public class AddAccountDialog extends AlertDialog {
protected AddAccountDialog(@NonNull Context context, int themeResId) {
super(context, themeResId);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}

View File

@ -1,158 +0,0 @@
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;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.textfield.TextInputEditText;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.persistence.entity.AccountModel;
import org.mercury_im.messenger.util.TextChangedListener;
import butterknife.BindView;
import butterknife.ButterKnife;
/**
* A login screen that offers login via email/password.
*/
public class LoginActivity extends AppCompatActivity implements TextView.OnEditorActionListener {
// UI references.
@BindView(R.id.jid)
TextInputEditText mJidView;
@BindView(R.id.password)
TextInputEditText mPasswordView;
@BindView(R.id.sign_in_button)
Button mSignInView;
private LoginViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
MercuryImApplication.getApplication().getAppComponent().inject(this);
// Set up the login form.
ButterKnife.bind(this);
viewModel = new ViewModelProvider(this).get(LoginViewModel.class);
observeViewModel(viewModel);
displayCredentials(viewModel.getAccount());
mJidView.setOnEditorActionListener(this);
mPasswordView.setOnEditorActionListener(this);
mJidView.addTextChangedListener(new TextChangedListener() {
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
viewModel.onJidInputChanged(charSequence.toString());
Log.d("Mercury", "onTextChanged");
}
});
mPasswordView.addTextChangedListener(new TextChangedListener() {
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
viewModel.onPasswordInputChanged(charSequence.toString());
Log.d("Mercury", "onTextChanged");
}
});
mSignInView.setOnClickListener(view -> viewModel.loginDetailsEntered());
}
private void observeViewModel(LoginViewModel viewModel) {
viewModel.getJidError().observe(this, jidError -> {
if (jidError == null) return;
String errorMessage = null;
switch (jidError) {
case none:
break;
case emptyJid:
errorMessage = getResources().getString(R.string.error_field_required);
break;
case invalidJid:
errorMessage = getResources().getString(R.string.error_invalid_jid);
break;
case unknownJid:
errorMessage = "Unknown Jid!";
}
mJidView.setError(errorMessage);
});
viewModel.getPasswordError().observe(this, passwordError -> {
if (passwordError == null) return;
String errorMessage = null;
switch (passwordError) {
case none:
break;
case emptyPassword:
errorMessage = getResources().getString(R.string.error_field_required);
break;
case invalidPassword:
errorMessage = getResources().getString(R.string.error_invalid_password);
break;
case incorrectPassword:
errorMessage = getResources().getString(R.string.error_incorrect_password);
break;
}
mPasswordView.setError(errorMessage);
});
viewModel.getSigninSuccessful().observe(this, aBoolean -> {
if (Boolean.TRUE.equals(aBoolean)) {
finish();
}
});
}
private void displayCredentials(LiveData<AccountModel> account) {
account.observe(this, accountModel -> {
if (accountModel == null) {
return;
}
if (accountModel.getJid() != null) {
mJidView.setText(accountModel.getJid().toString());
}
if (accountModel.getPassword() != null) {
mPasswordView.setText(accountModel.getPassword());
}
});
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
switch (v.getId()) {
case R.id.jid:
if (actionId == EditorInfo.IME_ACTION_NEXT) {
mPasswordView.requestFocus();
return true;
}
break;
case R.id.password:
if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) {
viewModel.loginDetailsEntered();
return true;
}
}
return false;
}
}

View File

@ -1,153 +0,0 @@
package org.mercury_im.messenger.ui.login;
import static org.mercury_im.messenger.core.connection.MercuryConnection.TAG;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
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.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.core.centers.ConnectionCenter;
import org.mercury_im.messenger.persistence.entity.AccountModel;
import org.mercury_im.messenger.persistence.repository.AccountRepository;
import javax.inject.Inject;
public class LoginViewModel extends ViewModel {
@Inject
AccountRepository accountRepository;
@Inject
ConnectionCenter connectionCenter;
private String jid;
private String password;
private MutableLiveData<JidError> jidError = new MutableLiveData<>();
private MutableLiveData<PasswordError> passwordError = new MutableLiveData<>();
private MutableLiveData<AccountModel> account = new MutableLiveData<>();
private MutableLiveData<Boolean> signinSuccessful = new MutableLiveData<>();
public LoginViewModel() {
super();
MercuryImApplication.getApplication().getAppComponent().inject(this);
init(new AccountModel());
}
public LiveData<Boolean> getSigninSuccessful() {
return signinSuccessful;
}
public enum JidError {
none,
emptyJid,
invalidJid,
unknownJid
}
public enum PasswordError {
none,
emptyPassword,
invalidPassword,
incorrectPassword
}
public void onJidInputChanged(String input) {
this.jid = input;
}
public void onPasswordInputChanged(String input) {
this.password = input;
}
public LiveData<JidError> getJidError() {
return jidError;
}
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 jid or null
*/
private EntityBareJid asValidJidOrNull(String input) {
return JidCreate.entityBareFromOrNull(input);
}
private boolean isPasswordValid(String password) {
return !password.isEmpty();
}
public void init(@NonNull AccountModel account) {
this.account.setValue(account);
}
public MutableLiveData<AccountModel> getAccount() {
return account;
}
public void login() {
AccountModel account = getAccount().getValue();
if (account != null && account.getJid() != null && !TextUtils.isEmpty(account.getPassword())) {
accountRepository.upsert(account);
}
}
public void loginDetailsEntered() {
boolean loginIntact = true;
if (jid.isEmpty()) {
jidError.postValue(JidError.emptyJid);
loginIntact = false;
}
EntityBareJid bareJid = asValidJidOrNull(jid);
if (bareJid == null) {
jidError.postValue(JidError.invalidJid);
loginIntact = false;
}
if (!isPasswordValid(password)) {
passwordError.postValue(PasswordError.invalidPassword);
loginIntact = false;
}
if (loginIntact) {
AccountModel accountModel = new AccountModel();
accountModel.setEnabled(true);
accountModel.setJid(bareJid);
accountModel.setPassword(password);
Single<AccountModel> insert = accountRepository.upsert(accountModel);
insert.subscribe(new DisposableSingleObserver<AccountModel>() {
@Override
public void onSuccess(AccountModel inserted) {
Log.d(MercuryImApplication.TAG, "LoginActivity.loginDetailsEntered: Account " + inserted.getId() + " inserted.");
connectionCenter.createConnection(accountModel);
signinSuccessful.setValue(true);
}
@Override
public void onError(Throwable e) {
Log.e(TAG, "Could not insert new Account data.", e);
}
});
}
}
}

View File

@ -12,7 +12,6 @@ import android.widget.Toast;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;

View File

@ -6,17 +6,17 @@ import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import org.mercury_im.messenger.persistence.entity.ContactModel;
import org.mercury_im.messenger.persistence.repository.RosterRepository;
import org.mercury_im.messenger.data.repository.PeerRepository;
import org.mercury_im.messenger.entity.contact.Peer;
import javax.inject.Inject;
public class ContactListItemViewModel extends AndroidViewModel {
@Inject
RosterRepository contactRepository;
PeerRepository contactRepository;
private LiveData<ContactModel> contact;
private LiveData<Peer> contact;
@Inject
public ContactListItemViewModel(@NonNull Application application) {

View File

@ -11,9 +11,9 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.jxmpp.jid.EntityBareJid;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.persistence.entity.ContactModel;
import org.mercury_im.messenger.entity.contact.Peer;
import org.mercury_im.messenger.ui.avatar.AvatarDrawable;
import org.mercury_im.messenger.ui.chat.ChatActivity;
import org.mercury_im.messenger.ui.util.AbstractRecyclerViewAdapter;
import org.mercury_im.messenger.util.ColorUtil;
@ -24,7 +24,7 @@ import butterknife.BindView;
import butterknife.ButterKnife;
public class ContactListRecyclerViewAdapter
extends AbstractRecyclerViewAdapter<ContactModel, ContactListRecyclerViewAdapter.RosterItemViewHolder> {
extends AbstractRecyclerViewAdapter<Peer, ContactListRecyclerViewAdapter.RosterItemViewHolder> {
public ContactListRecyclerViewAdapter() {
super(new ContactDiffCallback());
@ -39,8 +39,8 @@ public class ContactListRecyclerViewAdapter
@Override
public void onBindViewHolder(@NonNull RosterItemViewHolder holder, int position) {
ContactModel model = getModelAt(position);
holder.bind(model);
Peer peer = getItemAt(position);
holder.bind(peer);
}
public class RosterItemViewHolder extends RecyclerView.ViewHolder {
@ -65,38 +65,38 @@ public class ContactListRecyclerViewAdapter
ButterKnife.bind(this, view);
}
void bind(ContactModel contactModel) {
String name = contactModel.getRostername();
nameView.setText(name != null ? name : contactModel.getEntity().getJid().getLocalpart().asUnescapedString());
EntityBareJid jid = contactModel.getEntity().getJid();
jidView.setText(jid.toString());
avatarView.setColorFilter(ColorUtil.consistentColor(jid.toString()));
void bind(Peer contact) {
String name = contact.getDisplayName();
String address = contact.getAddress();
nameView.setText(name);
jidView.setText(address);
avatarView.setImageDrawable(new AvatarDrawable(name, address));
view.setOnClickListener(view -> {
Intent intent = new Intent(context, ChatActivity.class);
intent.putExtra(ChatActivity.EXTRA_JID, jid.toString());
intent.putExtra(ChatActivity.EXTRA_ACCOUNT, contactModel.getEntity().getAccount().getId());
intent.putExtra(ChatActivity.EXTRA_JID, address);
intent.putExtra(ChatActivity.EXTRA_ACCOUNT, contact.getAccount().getId().toString());
context.startActivity(intent);
});
}
}
private static class ContactDiffCallback extends AbstractDiffCallback<ContactModel> {
private static class ContactDiffCallback extends AbstractDiffCallback<Peer> {
ContactDiffCallback() {
super(true);
}
@Override
public boolean areItemsTheSame(ContactModel oldItem, ContactModel newItem) {
public boolean areItemsTheSame(Peer oldItem, Peer newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(ContactModel oldItem, ContactModel newItem) {
public boolean areContentsTheSame(Peer oldItem, Peer newItem) {
return areItemsTheSame(oldItem, newItem) &&
Objects.equals(oldItem.getRostername(), newItem.getRostername());
Objects.equals(oldItem.getName(), newItem.getName());
}
}
}

View File

@ -7,24 +7,22 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import org.mercury_im.messenger.MercuryImApplication;
import org.mercury_im.messenger.persistence.entity.ContactModel;
import org.mercury_im.messenger.persistence.repository.RosterRepository;
import org.mercury_im.messenger.data.repository.XmppPeerRepository;
import org.mercury_im.messenger.entity.contact.Peer;
import java.util.List;
import javax.inject.Inject;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
public class ContactListViewModel extends ViewModel {
@Inject
RosterRepository rosterRepository;
XmppPeerRepository xmppContactRepository;
private final MutableLiveData<List<ContactModel>> rosterEntryList = new MutableLiveData<>();
private final MutableLiveData<List<Peer>> rosterEntryList = new MutableLiveData<>();
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
public ContactListViewModel() {
@ -32,14 +30,8 @@ public class ContactListViewModel extends ViewModel {
MercuryImApplication.getApplication().getAppComponent().inject(this);
Log.d("ContactListViewModel", "Start observing database");
// Subscribe to changes to the contacts table and update the LiveData object for the UI.
compositeDisposable.add(rosterRepository.getAllContacts()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(o -> {
List<ContactModel> list = o.toList();
Log.d("ContactListViewModel", "Room changed contacts: " + list.size());
rosterEntryList.setValue(list);
}));
compositeDisposable.add(xmppContactRepository.observeAllPeers()
.subscribe(rosterEntryList::setValue));
}
@Override
@ -48,7 +40,7 @@ public class ContactListViewModel extends ViewModel {
compositeDisposable.clear();
}
public LiveData<List<ContactModel>> getRosterEntryList() {
public LiveData<List<Peer>> getRosterEntryList() {
return rosterEntryList;
}
}

View File

@ -155,7 +155,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
/**
* This method stops fragment injection in malicious applications.
* Make sure to deny any unknown fragments here.
* Make sure to deny any unknownUsername fragments here.
*/
protected boolean isValidFragment(String fragmentName) {
return PreferenceFragment.class.getName().equals(fragmentName)

View File

@ -27,7 +27,7 @@ public abstract class AbstractRecyclerViewAdapter<M, VH extends RecyclerView.Vie
return models;
}
public M getModelAt(int position) {
public M getItemAt(int position) {
return getModels().get(position);
}

View File

@ -10,7 +10,7 @@
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".ui.login.LoginActivity">
tools:context=".ui.account.LoginActivity">
<LinearLayout
android:id="@+id/jid_login_form"
@ -24,7 +24,7 @@
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/jid"
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/prompt_jid"

View File

@ -14,7 +14,7 @@
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
app:layoutManager="LinearLayoutManager"
tools:context="org.mercury_im.messenger.ui.login.AccountsFragment"
tools:context="org.mercury_im.messenger.ui.account.AccountsFragment"
tools:listitem="@layout/list_item_account" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton

View File

@ -10,7 +10,7 @@
<string name="prompt_password">Password</string>
<string name="action_sign_in">Sign in</string>
<string name="action_sign_in_short">Sign in</string>
<string name="error_invalid_jid">This XMPP address is invalid</string>
<string name="error_invalid_username">This XMPP address is invalid</string>
<string name="error_invalid_password">This password is too short</string>
<string name="error_incorrect_password">This password is incorrect</string>
<string name="error_field_required">This field is required</string>

View File

@ -1,14 +1,13 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.1'
classpath 'com.android.tools.build:gradle:3.5.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@ -30,7 +29,7 @@ allprojects {
// Smack nightly unique snapshots repo
//maven {
// url 'https://igniterealtime.org/repo/'
// }
// }
}
configurations {

View File

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

View File

@ -0,0 +1,18 @@
package org.mercury_im.domain.data.util;
import org.mercury_im.messenger.entity.contact.Peer;
public class ContactNameUtil {
public static String displayableNameFrom(Peer contact) {
if (contact == null) {
return null;
}
if (contact.getName() != null) {
return contact.getName();
}
return contact.getAddress();
}
}

View File

@ -1,7 +1,7 @@
package org.mercury_im.messenger.core;
import org.mercury_im.messenger.persistence.util.ChatAndPossiblyContact;
import org.mercury_im.messenger.xmpp.util.ChatAndPossiblyContact;
public interface NotificationManager {

View File

@ -11,10 +11,10 @@ import org.jivesoftware.smackx.csi.ClientStateIndicationManager;
import org.jivesoftware.smackx.mam.MamManager;
import org.mercury_im.messenger.core.connection.MercuryConfiguration;
import org.mercury_im.messenger.core.connection.MercuryConnection;
import org.mercury_im.messenger.persistence.entity.AccountModel;
import org.mercury_im.messenger.persistence.entity.ChatModel;
import org.mercury_im.messenger.persistence.repository.AccountRepository;
import org.mercury_im.messenger.persistence.repository.RosterRepository;
import org.mercury_im.messenger.xmpp.model.AccountModel;
import org.mercury_im.messenger.xmpp.model.ChatModel;
import org.mercury_im.messenger.xmpp.repository.RequeryAccountRepository;
import org.mercury_im.messenger.xmpp.repository.RosterRepository;
import org.mercury_im.messenger.core.stores.EntityCapsStore;
import org.mercury_im.messenger.core.stores.PlainMessageStore;
import org.mercury_im.messenger.core.stores.RosterStore;
@ -41,7 +41,7 @@ public class ConnectionCenter {
private static final Logger LOGGER = Logger.getLogger(ConnectionCenter.class.getName());
// Injected
private final AccountRepository accountRepository;
private final RequeryAccountRepository accountRepository;
private final RosterRepository rosterRepository;
private final PlainMessageStore messageStore;
private final EntityCapsStore entityCapsStore;
@ -58,7 +58,7 @@ public class ConnectionCenter {
@Inject
public ConnectionCenter(EntityCapsStore capsStore,
PlainMessageStore messageStore,
AccountRepository accountRepository,
RequeryAccountRepository accountRepository,
RosterRepository rosterRepository) {
LOGGER.log(Level.INFO, "ConnectionCenter initialized");
this.entityCapsStore = capsStore;

View File

@ -2,11 +2,6 @@ package org.mercury_im.messenger.core.di;
import org.mercury_im.messenger.core.NotificationManager;
import org.mercury_im.messenger.core.centers.ConnectionCenter;
import org.mercury_im.messenger.persistence.repository.AccountRepository;
import org.mercury_im.messenger.persistence.repository.ChatRepository;
import org.mercury_im.messenger.persistence.repository.EntityCapsRepository;
import org.mercury_im.messenger.persistence.repository.MessageRepository;
import org.mercury_im.messenger.persistence.repository.RosterRepository;
import org.mercury_im.messenger.core.stores.EntityCapsStore;
import org.mercury_im.messenger.core.stores.PlainMessageStore;

View File

@ -1,6 +1,6 @@
package org.mercury_im.messenger.core.di;
import org.mercury_im.messenger.persistence.di.RequeryModule;
import org.mercury_im.messenger.xmpp.di.RequeryModule;
import javax.inject.Singleton;

View File

@ -4,13 +4,12 @@ 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.persistence.entity.EntityCapsModel;
import org.mercury_im.messenger.persistence.repository.EntityCapsRepository;
import org.mercury_im.messenger.data.model.EntityCapsModel;
import org.mercury_im.messenger.data.repository.XmppEntityCapsRepository;
import java.io.StringReader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
@ -24,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

@ -14,15 +14,15 @@ import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.mercury_im.messenger.core.NotificationManager;
import org.mercury_im.messenger.core.connection.MercuryConnection;
import org.mercury_im.messenger.persistence.entity.ChatModel;
import org.mercury_im.messenger.persistence.entity.ContactModel;
import org.mercury_im.messenger.persistence.entity.EntityModel;
import org.mercury_im.messenger.persistence.entity.LastChatMessageRelation;
import org.mercury_im.messenger.persistence.entity.MessageModel;
import org.mercury_im.messenger.persistence.repository.ChatRepository;
import org.mercury_im.messenger.persistence.repository.MessageRepository;
import org.mercury_im.messenger.persistence.repository.RosterRepository;
import org.mercury_im.messenger.persistence.util.ChatAndPossiblyContact;
import org.mercury_im.messenger.xmpp.model.ChatModel;
import org.mercury_im.messenger.xmpp.model.ContactModel;
import org.mercury_im.messenger.xmpp.model.EntityModel;
import org.mercury_im.messenger.xmpp.model.LastChatMessageRelation;
import org.mercury_im.messenger.xmpp.model.MessageModel;
import org.mercury_im.messenger.xmpp.repository.ChatRepository;
import org.mercury_im.messenger.xmpp.repository.MessageRepository;
import org.mercury_im.messenger.xmpp.repository.RosterRepository;
import org.mercury_im.messenger.xmpp.util.ChatAndPossiblyContact;
import java.util.ArrayList;
import java.util.Date;

View File

@ -2,12 +2,12 @@ package org.mercury_im.messenger.core.stores;
import org.jivesoftware.smack.roster.packet.RosterPacket;
import org.jxmpp.jid.Jid;
import org.mercury_im.messenger.persistence.entity.AccountModel;
import org.mercury_im.messenger.persistence.entity.ContactModel;
import org.mercury_im.messenger.persistence.entity.EntityModel;
import org.mercury_im.messenger.persistence.repository.AccountRepository;
import org.mercury_im.messenger.persistence.repository.RosterRepository;
import org.mercury_im.messenger.persistence.enums.SubscriptionDirection;
import org.mercury_im.messenger.xmpp.model.AccountModel;
import org.mercury_im.messenger.xmpp.model.ContactModel;
import org.mercury_im.messenger.xmpp.model.EntityModel;
import org.mercury_im.messenger.xmpp.repository.RequeryAccountRepository;
import org.mercury_im.messenger.xmpp.repository.RosterRepository;
import org.mercury_im.messenger.xmpp.enums.SubscriptionDirection;
import java.util.ArrayList;
import java.util.Arrays;
@ -20,9 +20,7 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Action;
import io.reactivex.schedulers.Schedulers;
public class RosterStore implements org.jivesoftware.smack.roster.rosterstore.RosterStore {
@ -30,7 +28,7 @@ public class RosterStore implements org.jivesoftware.smack.roster.rosterstore.Ro
private static final Logger LOGGER = Logger.getLogger(RosterStore.class.getName());
private final RosterRepository rosterRepository;
private final AccountRepository accountRepository;
private final RequeryAccountRepository accountRepository;
private AccountModel account;
private CompositeDisposable disposable = null;
@ -38,7 +36,7 @@ public class RosterStore implements org.jivesoftware.smack.roster.rosterstore.Ro
private String rosterVersion;
@Inject
public RosterStore(RosterRepository rosterRepository, AccountRepository accountRepository) {
public RosterStore(RosterRepository rosterRepository, RequeryAccountRepository accountRepository) {
this.rosterRepository = rosterRepository;
this.accountRepository = accountRepository;
}

View File

@ -1,28 +0,0 @@
package org.mercury_im.messenger.core.util;
import org.mercury_im.messenger.persistence.entity.ContactModel;
import org.mercury_im.messenger.persistence.entity.EntityModel;
public class ContactNameUtil {
public static String displayableNameFrom(ContactModel contactModel) {
if (contactModel == null) {
return null;
}
if (contactModel.getRostername() != null) {
return contactModel.getRostername();
}
if (contactModel.getEntity() != null) {
return contactModel.getEntity().getJid().getLocalpart().asUnescapedString();
}
return null;
}
public static String displayableNameFrom(ContactModel contact, EntityModel entity) {
if (contact == null) {
return entity.getJid().getLocalpart().asUnescapedString();
} return displayableNameFrom(contact);
}
}

View File

@ -4,19 +4,24 @@ apply plugin: 'java-library'
// This is apparently necessary for use with requery.
sourceSets {
main.java.srcDirs += "${buildDir}/generated/sources/annotationProcessor/java/main/"
test.java.srcDirs += "${buildDir}/generated/sources/annotationProcessor/java/test/"
}
dependencies {
// JXMPP for Jid types. Version comes from smacks version.gradle
api("org.jxmpp:jxmpp-jid:$jxmppVersion")
implementation project(":entity")
implementation project(':domain')
compileOnly 'org.projectlombok:lombok:1.18.10'
annotationProcessor 'org.projectlombok:lombok:1.18.10'
// RxJava2
api "io.reactivex.rxjava2:rxjava:$rxJava2Version"
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"
// Requery ORM
api "io.requery:requery:$requeryVersion"
@ -24,7 +29,8 @@ dependencies {
// JUnit for testing
testImplementation "junit:junit:$junitVersion"
implementation project(':thread_utils')
testImplementation 'org.xerial:sqlite-jdbc:3.30.1'
}
sourceCompatibility = "8"

View File

@ -0,0 +1,44 @@
package org.mercury_im.messenger.data.converter;
import org.mercury_im.messenger.entity.message.MessageDirection;
import io.requery.Converter;
public class MessageDirectionConverter implements Converter<MessageDirection, Integer> {
@Override
public Class<MessageDirection> getMappedType() {
return MessageDirection.class;
}
@Override
public Class<Integer> getPersistedType() {
return Integer.class;
}
@Override
public Integer getPersistedSize() {
return null;
}
@Override
public Integer convertToPersisted(MessageDirection value) {
switch (value) {
case outgoing:
return 0;
case incoming:
return 1;
default: return 1;
}
}
@Override
public MessageDirection convertToMapped(Class<? extends MessageDirection> type, Integer value) {
switch (value) {
case 0:
return MessageDirection.outgoing;
case 1:
return MessageDirection.incoming;
default: return MessageDirection.incoming;
}
}
}

View File

@ -1,6 +1,6 @@
package org.mercury_im.messenger.persistence.converter;
package org.mercury_im.messenger.data.converter;
import org.mercury_im.messenger.persistence.enums.SaslCondition;
import org.mercury_im.messenger.data.enums.SaslCondition;
import io.requery.Converter;

View File

@ -1,6 +1,6 @@
package org.mercury_im.messenger.persistence.converter;
package org.mercury_im.messenger.data.converter;
import org.mercury_im.messenger.persistence.enums.SubscriptionDirection;
import org.mercury_im.messenger.entity.contact.SubscriptionDirection;
import io.requery.Converter;

View File

@ -0,0 +1,41 @@
package org.mercury_im.messenger.data.di;
import org.mercury_im.messenger.data.repository.dao.AccountDao;
import org.mercury_im.messenger.data.repository.dao.DirectChatDao;
import org.mercury_im.messenger.data.repository.dao.GroupChatDao;
import org.mercury_im.messenger.data.repository.dao.MessageDao;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import io.requery.Persistable;
import io.requery.reactivex.ReactiveEntityStore;
@Module
public class DaoModule {
@Provides
@Singleton
static AccountDao provideAccountDao(ReactiveEntityStore<Persistable> data) {
return new AccountDao(data);
}
@Provides
@Singleton
static DirectChatDao provideDirectChatDao(ReactiveEntityStore<Persistable> data) {
return new DirectChatDao(data);
}
@Provides
@Singleton
static GroupChatDao provideGroupChatDao(ReactiveEntityStore<Persistable> data) {
return new GroupChatDao(data);
}
@Provides
@Singleton
static MessageDao provideMessageDao(ReactiveEntityStore<Persistable> data) {
return new MessageDao(data);
}
}

View File

@ -0,0 +1,67 @@
package org.mercury_im.messenger.data.di;
import org.mercury_im.messenger.data.mapping.AccountMapping;
import org.mercury_im.messenger.data.mapping.DirectChatMapping;
import org.mercury_im.messenger.data.mapping.EntityCapsMapping;
import org.mercury_im.messenger.data.mapping.GroupChatMapping;
import org.mercury_im.messenger.data.mapping.MessagePayloadMapping;
import org.mercury_im.messenger.data.mapping.MessageMapping;
import org.mercury_im.messenger.data.mapping.MessagePayloadContainerMapping;
import org.mercury_im.messenger.data.mapping.PeerMapping;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class MappingModule {
@Provides
@Singleton
static AccountMapping provideAccountMapping() {
return new AccountMapping();
}
@Provides
@Singleton
static PeerMapping providePeerMapping() {
return new PeerMapping(provideAccountMapping());
}
@Provides
@Singleton
static DirectChatMapping provideDirectChatMapping() {
return new DirectChatMapping(providePeerMapping());
}
@Provides
@Singleton
static GroupChatMapping provideGroupChatMapping() {
return new GroupChatMapping(provideAccountMapping());
}
@Provides
@Singleton
static MessageMapping provideMessageMapping() {
return new MessageMapping(provideMessagePayloadMapping());
}
@Provides
@Singleton
static MessagePayloadContainerMapping provideMessagePayloadMapping() {
return new MessagePayloadContainerMapping(provideMessageContentMapping());
}
@Provides
@Singleton
static MessagePayloadMapping provideMessageContentMapping() {
return new MessagePayloadMapping();
}
@Provides
@Singleton
static EntityCapsMapping provideEntityCapsMapping() {
return new EntityCapsMapping();
}
}

View File

@ -0,0 +1,115 @@
package org.mercury_im.messenger.data.di;
import org.mercury_im.messenger.data.mapping.AccountMapping;
import org.mercury_im.messenger.data.mapping.DirectChatMapping;
import org.mercury_im.messenger.data.mapping.EntityCapsMapping;
import org.mercury_im.messenger.data.mapping.GroupChatMapping;
import org.mercury_im.messenger.data.mapping.MessageMapping;
import org.mercury_im.messenger.data.mapping.PeerMapping;
import org.mercury_im.messenger.data.repository.AccountRepository;
import org.mercury_im.messenger.data.repository.EntityCapsRepository;
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.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;
import org.mercury_im.messenger.data.repository.XmppGroupChatRepository;
import org.mercury_im.messenger.data.repository.XmppMessageRepository;
import org.mercury_im.messenger.data.repository.XmppPeerRepository;
import org.mercury_im.messenger.util.ThreadUtils;
import javax.inject.Named;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import io.reactivex.Scheduler;
import io.requery.Persistable;
import io.requery.reactivex.ReactiveEntityStore;
@Module(includes = {
MappingModule.class,
DaoModule.class
})
public class RepositoryModule {
@Provides
@Singleton
static AccountRepository provideAccountRepository(
ReactiveEntityStore<Persistable> data,
@Named(value = ThreadUtils.SCHEDULER_IO) Scheduler ioScheduler,
@Named(value = ThreadUtils.SCHEDULER_UI) Scheduler uiScheduler,
AccountMapping accountMapping) {
return new XmppAccountRepository(data, ioScheduler, uiScheduler, accountMapping);
}
@Provides
@Singleton
static PeerRepository providePeerRepository(
ReactiveEntityStore<Persistable> data,
@Named(value = ThreadUtils.SCHEDULER_IO) Scheduler ioScheduler,
@Named(value = ThreadUtils.SCHEDULER_UI) Scheduler uiScheduler,
PeerMapping peerMapping,
AccountRepository accountRepository) {
return new XmppPeerRepository(data, ioScheduler, uiScheduler, peerMapping, accountRepository);
}
@Provides
@Singleton
static DirectChatRepository provideDirectChatRepository(
ReactiveEntityStore<Persistable> data,
@Named(value = ThreadUtils.SCHEDULER_IO) Scheduler ioScheduler,
@Named(value = ThreadUtils.SCHEDULER_UI) Scheduler uiScheduler,
DirectChatMapping directChatMapping) {
return new XmppDirectChatRepository(data, ioScheduler, uiScheduler, directChatMapping);
}
@Provides
@Singleton
static GroupChatRepository provideGroupChatRepository(
ReactiveEntityStore<Persistable> data,
@Named(value = ThreadUtils.SCHEDULER_IO) Scheduler ioScheduler,
@Named(value = ThreadUtils.SCHEDULER_UI) Scheduler uiScheduler,
GroupChatMapping groupChatMapping) {
return new XmppGroupChatRepository(data, ioScheduler, uiScheduler, groupChatMapping);
}
@Provides
@Singleton
static MessageRepository provideMessageRepository(
ReactiveEntityStore<Persistable> data,
@Named(value = ThreadUtils.SCHEDULER_IO) Scheduler ioScheduler,
@Named(value = ThreadUtils.SCHEDULER_UI) Scheduler uiScheduler,
MessageMapping messageMapping,
DirectChatMapping directChatMapping,
GroupChatMapping groupChatMapping) {
return new XmppMessageRepository(data, ioScheduler, uiScheduler,
messageMapping, directChatMapping, groupChatMapping);
}
@Provides
@Singleton
static EntityCapsRepository provideCapsRepository(
ReactiveEntityStore<Persistable> data,
@Named(value = ThreadUtils.SCHEDULER_IO) Scheduler ioScheduler,
@Named(value = ThreadUtils.SCHEDULER_UI) Scheduler uiScheduler,
EntityCapsMapping entityCapsMapping) {
return new XmppEntityCapsRepository(data, ioScheduler, uiScheduler, entityCapsMapping);
}
@Provides
@Singleton
static Repositories provideRepositories(
AccountRepository accountRepository,
DirectChatRepository directChatRepository,
GroupChatRepository groupChatRepository,
MessageRepository messageRepository,
PeerRepository peerRepository,
XmppEntityCapsRepository entityCapsRepository) {
return new Repositories(accountRepository, directChatRepository, groupChatRepository,
messageRepository, peerRepository, entityCapsRepository);
}
}

View File

@ -0,0 +1,8 @@
package org.mercury_im.messenger.data.enums;
public enum MessageContentType {
/**
* Content is a message body.
*/
body
}

View File

@ -1,4 +1,4 @@
package org.mercury_im.messenger.persistence.enums;
package org.mercury_im.messenger.data.enums;
public enum SaslCondition {
@ -59,12 +59,12 @@ public enum SaslCondition {
incorrect_encoding,
/**
* The authzid provided by the initiating entity is invalid, either because it is incorrectly
* The authzid provided by the initiating entity is invalidUsername, either because it is incorrectly
* formatted or because the initiating entity does not have permissions to authorize that ID;
* sent in reply to a <pre><response/></pre> element or an <pre><auth/></pre> element with
* initial response data.
*
* @see <a href="https://xmpp.org/rfcs/rfc6120.html#sasl-errors-invalid-authzid">rfc6120 §6.5.6: invalid-authzid</a>
* @see <a href="https://xmpp.org/rfcs/rfc6120.html#sasl-errors-invalid-authzid">rfc6120 §6.5.6: invalidUsername-authzid</a>
*/
invalid_authzid,
@ -72,7 +72,7 @@ public enum SaslCondition {
* The initiating entity did not specify a mechanism, or requested a mechanism that is not
* supported by the receiving entity; sent in reply to an <pre><auth/></pre> element.
*
* @see <a href="https://xmpp.org/rfcs/rfc6120.html#sasl-errors-invalid-mechanism">rfc6120 §6.5.7: invalid-mechanism</a>
* @see <a href="https://xmpp.org/rfcs/rfc6120.html#sasl-errors-invalid-mechanism">rfc6120 §6.5.7: invalidUsername-mechanism</a>
*/
invalid_mechanism,

View File

@ -0,0 +1,96 @@
package org.mercury_im.messenger.data.mapping;
import org.mercury_im.messenger.util.Optional;
import lombok.NonNull;
public abstract class AbstractMapping<E, M> implements Mapping<E, M> {
@Override
public M toModel(E entity) {
if (entity == null) {
return null;
}
return toModel(entity, newModel(entity));
}
@Override
public Optional<M> toModel(Optional<E> entity) {
if (entity.isPresent()) {
return new Optional<>(toModel(entity.getItem()));
} else {
return new Optional<>();
}
}
@Override
public E toEntity(M model) {
if (model == null) {
return null;
}
return toEntity(model, newEntity(model));
}
@Override
public Optional<E> toEntity(Optional<M> model) {
if (model.isPresent()) {
return new Optional<>(toEntity(model.getItem()));
} else {
return new Optional<>();
}
}
@Override
public M toModel(E entity, M model) {
if (entity == null) {
return null;
}
if (model == null) {
model = newModel(entity);
}
return mapToModel(entity, model);
}
@Override
public E toEntity(M model, E entity) {
if (model == null) {
return null;
}
if (entity == null) {
entity = newEntity(model);
}
return mapToEntity(model, entity);
}
/**
* Return a new entity object.
*
* @return entity
*/
protected abstract E newEntity(@NonNull M model);
/**
* Return a new database model object.
*
* @return model
*/
protected abstract M newModel(@NonNull E entity);
/**
* Copy data from the entity to the given model.
*
* @param entity application entity
* @param model database model
* @return the database model
*/
protected abstract M mapToModel(@NonNull E entity, @NonNull M model);
/**
* Copy data from the database model to the entity.
*
* @param model database model
* @param entity entity
* @return the application entity
*/
protected abstract E mapToEntity(@NonNull M model, @NonNull E entity);
}

View File

@ -0,0 +1,49 @@
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 javax.inject.Inject;
public class AccountMapping extends AbstractMapping<Account, AccountModel> {
@Inject
public AccountMapping() {
}
@Override
public Account newEntity(AccountModel model) {
return new IAccount();
}
@Override
public AccountModel newModel(Account entity) {
return new AccountModel();
}
@Override
public AccountModel mapToModel(Account entity, AccountModel model) {
model.setId(entity.getId());
model.setAddress(entity.getAddress());
model.setPassword(entity.getPassword());
model.setHost(entity.getHost());
model.setPort(entity.getPort());
model.setEnabled(entity.isEnabled());
return model;
}
@Override
public Account mapToEntity(AccountModel model, Account entity) {
entity.setId(model.getId());
entity.setAddress(model.getAddress());
entity.setPassword(model.getPassword());
entity.setHost(model.getHost());
entity.setPort(model.getPort());
entity.setEnabled(model.isEnabled());
return entity;
}
}

View File

@ -0,0 +1,44 @@
package org.mercury_im.messenger.data.mapping;
import org.mercury_im.messenger.data.model.DirectChatModel;
import org.mercury_im.messenger.entity.chat.DirectChat;
import org.mercury_im.messenger.entity.chat.IDirectChat;
import org.mercury_im.messenger.entity.contact.Peer;
import javax.inject.Inject;
public class DirectChatMapping extends AbstractMapping<DirectChat, DirectChatModel> {
private final PeerMapping peerMapping;
@Inject
public DirectChatMapping(PeerMapping peerMapping) {
this.peerMapping = peerMapping;
}
@Override
public DirectChat newEntity(DirectChatModel model) {
return new IDirectChat();
}
@Override
public DirectChatModel newModel(DirectChat entity) {
return new DirectChatModel();
}
@Override
public DirectChatModel mapToModel(DirectChat entity, DirectChatModel model) {
model.setId(entity.getId());
model.setPeer(peerMapping.toModel(entity.getPeer(), model.getPeer()));
return model;
}
@Override
public DirectChat mapToEntity(DirectChatModel model, DirectChat entity) {
entity.setId(model.getId());
Peer peer = peerMapping.toEntity(model.getPeer(), entity.getPeer());
entity.setPeer(peer);
entity.setAccount(peer.getAccount());
return entity;
}
}

View File

@ -0,0 +1,41 @@
package org.mercury_im.messenger.data.mapping;
import org.mercury_im.messenger.data.model.EntityCapsModel;
import org.mercury_im.messenger.entity.caps.EntityCapsRecord;
import org.mercury_im.messenger.entity.caps.IEntityCapsRecord;
import javax.inject.Inject;
import lombok.NonNull;
public class EntityCapsMapping extends AbstractMapping<EntityCapsRecord, EntityCapsModel> {
@Inject
public EntityCapsMapping() {
}
@Override
protected EntityCapsRecord newEntity(@NonNull EntityCapsModel model) {
return new IEntityCapsRecord();
}
@Override
protected EntityCapsModel newModel(@NonNull EntityCapsRecord entity) {
return new EntityCapsModel();
}
@Override
protected EntityCapsModel mapToModel(@NonNull EntityCapsRecord entity, @NonNull EntityCapsModel model) {
model.setNodeVer(entity.getNodeVer());
model.setXml(entity.getXml());
return model;
}
@Override
protected EntityCapsRecord mapToEntity(@NonNull EntityCapsModel model, @NonNull EntityCapsRecord entity) {
entity.setNodeVer(model.getNodeVer());
entity.setXml(model.getXml());
return entity;
}
}

View File

@ -0,0 +1,44 @@
package org.mercury_im.messenger.data.mapping;
import org.mercury_im.messenger.data.model.GroupChatModel;
import org.mercury_im.messenger.entity.chat.GroupChat;
import org.mercury_im.messenger.entity.chat.IGroupChat;
import javax.inject.Inject;
public class GroupChatMapping extends AbstractMapping<GroupChat, GroupChatModel> {
private final AccountMapping accountMapping;
@Inject
public GroupChatMapping(AccountMapping accountMapping) {
this.accountMapping = accountMapping;
}
@Override
public GroupChat newEntity(GroupChatModel model) {
return new IGroupChat();
}
@Override
public GroupChatModel newModel(GroupChat entity) {
return new GroupChatModel();
}
@Override
public GroupChatModel mapToModel(GroupChat entity, GroupChatModel model) {
model.setAccount(accountMapping.toModel(entity.getAccount(), model.getAccount()));
model.setAddress(entity.getRoomAddress());
model.setName(entity.getRoomName());
return model;
}
@Override
public GroupChat mapToEntity(GroupChatModel model, GroupChat entity) {
entity.setId(model.getId());
entity.setAccount(accountMapping.toEntity(model.getAccount(), entity.getAccount()));
entity.setRoomAddress(model.getAddress());
entity.setRoomName(model.getName());
return entity;
}
}

View File

@ -0,0 +1,51 @@
package org.mercury_im.messenger.data.mapping;
import org.mercury_im.messenger.util.Optional;
/**
* Interface that defines a mapping between entities and database models.
*
* @param <E> Entity
* @param <M> Model
*/
public interface Mapping<E, M> {
/**
* Map data from the entity to a new model.
*
* @param entity application entity
* @return new database model
*/
M toModel(E entity);
Optional<M> toModel(Optional<E> entity);
/**
* Copy data from the model to a new entity.
* @param model database model
* @return new application entity
*/
E toEntity(M model);
Optional<E> toEntity(Optional<M> model);
/**
* Map an entity to a model.
*
* @param entity entity
* @param model model
* @return model
*/
M toModel(E entity, M model);
/**
* Map a model to an entity.
*
* @param model model
* @param entity entity
* @return entity
*/
E toEntity(M model, E entity);
}

View File

@ -0,0 +1,65 @@
package org.mercury_im.messenger.data.mapping;
import org.mercury_im.messenger.data.model.MessageModel;
import org.mercury_im.messenger.data.model.MessagePayloadContainerModel;
import org.mercury_im.messenger.entity.message.IMessage;
import org.mercury_im.messenger.entity.message.Message;
import org.mercury_im.messenger.entity.message.PayloadContainer;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
public class MessageMapping extends AbstractMapping<Message, MessageModel> {
private final MessagePayloadContainerMapping messagePayloadContainerMapping;
@Inject
public MessageMapping(MessagePayloadContainerMapping messagePayloadContainerMapping) {
this.messagePayloadContainerMapping = messagePayloadContainerMapping;
}
@Override
public Message newEntity(MessageModel model) {
return new IMessage();
}
@Override
public MessageModel newModel(Message entity) {
return new MessageModel();
}
@Override
public MessageModel mapToModel(Message entity, MessageModel model) {
model.setSender(entity.getSender());
model.setRecipient(entity.getRecipient());
model.setTimestamp(entity.getTimestamp());
model.setDirection(entity.getDirection());
model.getPayloads().clear();
for (PayloadContainer payload : entity.getMessagePayloads()) {
MessagePayloadContainerModel payloadModel = messagePayloadContainerMapping.toModel(payload, new MessagePayloadContainerModel());
payloadModel.setMessage(model);
model.getPayloads().add(payloadModel);
}
return model;
}
@Override
public Message mapToEntity(MessageModel model, Message entity) {
entity.setId(model.getId());
entity.setSender(model.getSender());
entity.setRecipient(model.getRecipient());
entity.setTimestamp(model.getTimestamp());
entity.setDirection(model.getDirection());
List<PayloadContainer> payloadContainers = new ArrayList<>(entity.getMessagePayloads().size());
for (MessagePayloadContainerModel containerModel : model.getPayloads()) {
payloadContainers.add(messagePayloadContainerMapping.toEntity(containerModel));
}
entity.setMessagePayloads(payloadContainers);
return entity;
}
}

View File

@ -0,0 +1,58 @@
package org.mercury_im.messenger.data.mapping;
import org.mercury_im.messenger.data.model.MessagePayloadModel;
import org.mercury_im.messenger.data.model.MessagePayloadContainerModel;
import org.mercury_im.messenger.entity.message.IPayloadContainer;
import org.mercury_im.messenger.entity.message.PayloadContainer;
import org.mercury_im.messenger.entity.message.content.Payload;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
public class MessagePayloadContainerMapping extends AbstractMapping<PayloadContainer, MessagePayloadContainerModel> {
private final MessagePayloadMapping messagePayloadMapping;
@Inject
public MessagePayloadContainerMapping(MessagePayloadMapping messagePayloadMapping) {
this.messagePayloadMapping = messagePayloadMapping;
}
@Override
public PayloadContainer newEntity(MessagePayloadContainerModel model) {
return new IPayloadContainer();
}
@Override
public MessagePayloadContainerModel newModel(PayloadContainer entity) {
return new MessagePayloadContainerModel();
}
@Override
public MessagePayloadContainerModel mapToModel(PayloadContainer entity, MessagePayloadContainerModel model) {
model.getContents().clear();
for (Payload contentEntity : entity.getMessageContents()) {
MessagePayloadModel contentModel = messagePayloadMapping.toModel(contentEntity, new MessagePayloadModel());
contentModel.setPayloadContainer(model);
model.getContents().add(contentModel);
}
return model;
}
@Override
public PayloadContainer mapToEntity(MessagePayloadContainerModel model, PayloadContainer entity) {
entity.setId(model.getId());
List<Payload> contents = new ArrayList<>(model.getContents().size());
for (MessagePayloadModel contentModel : model.getContents()) {
Payload contentEntity = messagePayloadMapping.toEntity(contentModel, null);
contents.add(contentEntity);
}
entity.setMessageContents(contents);
return entity;
}
}

View File

@ -0,0 +1,47 @@
package org.mercury_im.messenger.data.mapping;
import org.mercury_im.messenger.data.model.MessagePayloadModel;
import org.mercury_im.messenger.entity.message.content.Payload;
import org.mercury_im.messenger.entity.message.content.TextPayload;
import static org.mercury_im.messenger.data.enums.MessageContentType.body;
public class MessagePayloadMapping extends AbstractMapping<Payload, MessagePayloadModel> {
@Override
public Payload newEntity(MessagePayloadModel model) {
switch (model.getType()) {
case body:
return new TextPayload();
default:
return null;
}
}
@Override
public MessagePayloadModel newModel(Payload entity) {
return new MessagePayloadModel();
}
@Override
public MessagePayloadModel mapToModel(Payload entity, MessagePayloadModel model) {
if (entity instanceof TextPayload) {
model.setType(body);
model.setBody(((TextPayload) entity).getBody());
}
// else if (...)
return model;
}
@Override
public Payload mapToEntity(MessagePayloadModel model, Payload entity) {
entity.setId(model.getId());
switch (model.getType()) {
case body:
TextPayload body = (TextPayload) entity;
body.setBody(model.getBody());
break;
}
return entity;
}
}

View File

@ -0,0 +1,55 @@
package org.mercury_im.messenger.data.mapping;
import org.mercury_im.messenger.data.model.PeerModel;
import org.mercury_im.messenger.entity.contact.IPeer;
import org.mercury_im.messenger.entity.contact.Peer;
import javax.inject.Inject;
public class PeerMapping extends AbstractMapping<Peer, PeerModel> {
private final AccountMapping accountMapping;
@Inject
public PeerMapping(AccountMapping accountMapping) {
this.accountMapping = accountMapping;
}
@Override
public Peer newEntity(PeerModel model) {
return new IPeer();
}
@Override
public PeerModel newModel(Peer entity) {
return new PeerModel();
}
@Override
public PeerModel mapToModel(Peer entity, PeerModel model) {
model.setId(entity.getId());
model.setAccount(accountMapping.toModel(entity.getAccount(), model.getAccount()));
model.setAddress(entity.getAddress());
model.setName(entity.getName());
model.setSubscriptionDirection(entity.getSubscriptionDirection());
model.setSubscriptionPending(entity.isSubscriptionPending());
model.setSubscriptionPreApproved(entity.isSubscriptionApproved());
return model;
}
@Override
public Peer mapToEntity(PeerModel model, Peer entity) {
entity.setId(model.getId());
entity.setAccount(accountMapping.toEntity(model.getAccount(), entity.getAccount()));
entity.setAddress(model.getAddress());
entity.setName(model.getName());
entity.setSubscriptionDirection(model.getSubscriptionDirection());
entity.setSubscriptionPending(model.isSubscriptionPending());
entity.setSubscriptionApproved(model.isSubscriptionPreApproved());
return entity;
}
}

View File

@ -0,0 +1,11 @@
/**
* The mapping package contains mapper classes that map database models to entities.
* The models can be found in {@link org.mercury_im.messenger.data.model} in this module,
* while the entity classes are located in the <pre>entity</pre> module.
*
* The mapper classes define an architectural boundary that separates the entities from
* their database model counterparts. This is done in order to keep the database logic separated
* from the domain logic and to allow for quick replacement of the database implementation.
*
*/
package org.mercury_im.messenger.data.mapping;

View File

@ -1,7 +1,6 @@
package org.mercury_im.messenger.persistence.entity;
package org.mercury_im.messenger.data.model;
import org.jxmpp.jid.EntityBareJid;
import org.mercury_im.messenger.persistence.converter.EntityBareJidConverter;
import java.util.UUID;
import io.requery.Column;
import io.requery.Convert;
@ -10,29 +9,38 @@ import io.requery.Generated;
import io.requery.Key;
import io.requery.Persistable;
import io.requery.Table;
import io.requery.converter.UUIDConverter;
@Table(name = "accounts")
@Entity
public abstract class AbstractAccountModel implements Persistable {
@Key @Generated
long id;
@Key
@Convert(UUIDConverter.class)
UUID id;
@Column(nullable = false)
@Convert(EntityBareJidConverter.class)
EntityBareJid jid;
String address;
@Column(nullable = false)
String password;
@Column
String host;
@Column
int port;
@Column
boolean enabled;
@Column
String rosterVersion;
@Override
public String toString() {
return "Account[" + id + ", " +
jid + ", " +
address + ", " +
(enabled ? "enabled" : "disabled") + "]";
}
}

View File

@ -0,0 +1,29 @@
package org.mercury_im.messenger.data.model;
import java.util.UUID;
import io.requery.CascadeAction;
import io.requery.Convert;
import io.requery.Entity;
import io.requery.ForeignKey;
import io.requery.Generated;
import io.requery.Key;
import io.requery.OneToOne;
import io.requery.Persistable;
import io.requery.Table;
import io.requery.converter.UUIDConverter;
@Entity
@Table(name = "chats")
public abstract class AbstractDirectChatModel implements Persistable {
@Key
@Convert(UUIDConverter.class)
UUID id;
@OneToOne(cascade = CascadeAction.NONE)
@ForeignKey(referencedColumn = "id")
PeerModel peer;
boolean displayed;
}

View File

@ -0,0 +1,26 @@
package org.mercury_im.messenger.data.model;
import io.requery.CascadeAction;
import io.requery.Entity;
import io.requery.ForeignKey;
import io.requery.Generated;
import io.requery.Key;
import io.requery.ManyToOne;
import io.requery.ReferentialAction;
import io.requery.Table;
@Entity
@Table(name = "direct_messages")
public abstract class AbstractDirectMessagesRelation {
@Key @Generated
long id;
@ManyToOne(cascade = {CascadeAction.SAVE})
@ForeignKey(referencedColumn = "id", delete = ReferentialAction.CASCADE)
DirectChatModel chat;
@ManyToOne(cascade = {CascadeAction.SAVE, CascadeAction.DELETE})
@ForeignKey(referencedColumn = "id", delete = ReferentialAction.CASCADE)
MessageModel message;
}

View File

@ -1,4 +1,4 @@
package org.mercury_im.messenger.persistence.entity;
package org.mercury_im.messenger.data.model;
import io.requery.Column;
import io.requery.Entity;

View File

@ -0,0 +1,35 @@
package org.mercury_im.messenger.data.model;
import java.util.UUID;
import io.requery.Column;
import io.requery.Convert;
import io.requery.Entity;
import io.requery.Generated;
import io.requery.Key;
import io.requery.ManyToOne;
import io.requery.Persistable;
import io.requery.Table;
import io.requery.converter.UUIDConverter;
@Entity
@Table(name = "groupchats")
public abstract class AbstractGroupChatModel implements Persistable {
@Key
@Convert(UUIDConverter.class)
UUID id;
@Column(nullable = false)
@ManyToOne
AccountModel account;
@Column(nullable = false)
String address;
@Column
String name;
@Column
boolean auto_join;
}

View File

@ -0,0 +1,27 @@
package org.mercury_im.messenger.data.model;
import io.requery.CascadeAction;
import io.requery.Entity;
import io.requery.ForeignKey;
import io.requery.Generated;
import io.requery.Key;
import io.requery.ManyToOne;
import io.requery.OneToOne;
import io.requery.ReferentialAction;
import io.requery.Table;
@Entity
@Table(name = "group_messages")
public abstract class AbstractGroupMessagesRelation {
@Key @Generated
long id;
@ManyToOne(cascade = {CascadeAction.SAVE})
@ForeignKey(referencedColumn = "id", delete = ReferentialAction.CASCADE)
GroupChatModel chat;
@ManyToOne(cascade = {CascadeAction.SAVE, CascadeAction.DELETE})
@ForeignKey(referencedColumn = "id", delete = ReferentialAction.CASCADE)
MessageModel message;
}

View File

@ -0,0 +1,55 @@
package org.mercury_im.messenger.data.model;
import org.mercury_im.messenger.data.converter.MessageDirectionConverter;
import org.mercury_im.messenger.entity.message.MessageDirection;
import java.util.Date;
import java.util.Set;
import java.util.UUID;
import io.requery.Column;
import io.requery.Convert;
import io.requery.Entity;
import io.requery.Generated;
import io.requery.Key;
import io.requery.OneToMany;
import io.requery.Persistable;
import io.requery.Table;
import io.requery.converter.UUIDConverter;
@Entity
@Table(name = "messages")
public abstract class AbstractMessageModel implements Persistable {
@Key
@Convert(UUIDConverter.class)
UUID id;
@Column(nullable = false)
String sender;
@Column(nullable = false)
String recipient;
@Column(name = "\"timestamp\"", nullable = false)
Date timestamp;
@Column(nullable = false)
@Convert(MessageDirectionConverter.class)
MessageDirection direction;
@OneToMany
Set<MessagePayloadContainerModel> payloads;
@Column
String legacyId;
@Column
String originId;
@Column
String stanzaId;
@Column
String stanzaIdBy;
}

View File

@ -0,0 +1,24 @@
package org.mercury_im.messenger.data.model;
import java.util.Set;
import io.requery.Entity;
import io.requery.Generated;
import io.requery.Key;
import io.requery.ManyToOne;
import io.requery.OneToMany;
import io.requery.Table;
@Entity
@Table(name = "msg_payload_containers")
public abstract class AbstractMessagePayloadContainerModel {
@Key @Generated
long id;
@ManyToOne
MessageModel message;
@OneToMany
Set<MessagePayloadModel> contents;
}

View File

@ -0,0 +1,27 @@
package org.mercury_im.messenger.data.model;
import org.mercury_im.messenger.data.enums.MessageContentType;
import io.requery.Column;
import io.requery.Entity;
import io.requery.Generated;
import io.requery.Key;
import io.requery.ManyToOne;
import io.requery.Table;
@Entity
@Table(name = "msg_payloads")
public abstract class AbstractMessagePayloadModel {
@Key @Generated
long id;
@ManyToOne
MessagePayloadContainerModel payloadContainer;
@Column
String body;
@Column(nullable = false)
MessageContentType type;
}

View File

@ -0,0 +1,56 @@
package org.mercury_im.messenger.data.model;
import org.mercury_im.messenger.data.converter.SubscriptionDirectionConverter;
import org.mercury_im.messenger.entity.contact.SubscriptionDirection;
import java.util.UUID;
import io.requery.CascadeAction;
import io.requery.Column;
import io.requery.Convert;
import io.requery.Entity;
import io.requery.ForeignKey;
import io.requery.Generated;
import io.requery.Index;
import io.requery.Key;
import io.requery.ManyToOne;
import io.requery.Persistable;
import io.requery.ReferentialAction;
import io.requery.Table;
import io.requery.converter.UUIDConverter;
@Entity
@Table(name = "contacts", uniqueIndexes = "unique_address")
public abstract class AbstractPeerModel implements Persistable {
@Key
@Convert(UUIDConverter.class)
UUID id;
@Index("unique_address")
@ManyToOne(cascade = CascadeAction.NONE)
@ForeignKey(referencedColumn = "id")
AccountModel account;
@Index("unique_address")
@Column(nullable = false)
String address;
@Column
String name;
@Convert(SubscriptionDirectionConverter.class)
SubscriptionDirection subscriptionDirection;
boolean subscriptionPending;
boolean subscriptionPreApproved;
@Override
public String toString() {
return "Peer[" + id + ", " +
name + ", " +
address + ", " +
account + "]";
}
}

View File

@ -1,6 +1,6 @@
package org.mercury_im.messenger.persistence.entity;
package org.mercury_im.messenger.data.model;
import org.mercury_im.messenger.persistence.enums.SaslCondition;
import org.mercury_im.messenger.data.enums.SaslCondition;
import io.requery.Entity;
import io.requery.Key;

View File

@ -0,0 +1,12 @@
/**
* The model package contains requery model definitions.
* All of those classes are abstract, since the requery framework generates concrete implementations
* during compilation. Those files can later be found in
* <pre>build/generated/sources/annotationProcessor/java/main/...</pre>.
*
* The structure of the model classes closely mimics the structure of their entity pendants
* declared in the <pre>entity</pre> module.
*
* @see <a href="https://github.com/requery/requery/wiki/Defining-Entities">requery wiki on model definitions</a>
*/
package org.mercury_im.messenger.data.model;

View File

@ -1,4 +1,8 @@
package org.mercury_im.messenger.persistence.repository;
package org.mercury_im.messenger.data.repository;
import org.mercury_im.messenger.util.ThreadUtils;
import javax.inject.Named;
import io.reactivex.Scheduler;
import io.requery.Persistable;
@ -12,8 +16,8 @@ public abstract class RequeryRepository {
private final ReactiveEntityStore<Persistable> data;
protected RequeryRepository(ReactiveEntityStore<Persistable> data,
Scheduler subscriberScheduler,
Scheduler observerScheduler) {
@Named(value = ThreadUtils.SCHEDULER_IO) Scheduler subscriberScheduler,
@Named(value = ThreadUtils.SCHEDULER_UI) Scheduler observerScheduler) {
this.data = data;
this.subscriberScheduler = subscriberScheduler;
this.observerScheduler = observerScheduler;

View File

@ -0,0 +1,146 @@
package org.mercury_im.messenger.data.repository;
import org.mercury_im.messenger.data.mapping.AccountMapping;
import org.mercury_im.messenger.data.model.AccountModel;
import org.mercury_im.messenger.data.repository.dao.AccountDao;
import org.mercury_im.messenger.util.Optional;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.util.ThreadUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import javax.inject.Inject;
import javax.inject.Named;
import io.reactivex.Completable;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.Single;
import io.requery.Persistable;
import io.requery.query.ResultDelegate;
import io.requery.reactivex.ReactiveEntityStore;
import io.requery.reactivex.ReactiveResult;
public class XmppAccountRepository
extends RequeryRepository
implements AccountRepository {
private final AccountMapping accountMapping;
private final AccountDao dao;
@Inject
public XmppAccountRepository(ReactiveEntityStore<Persistable> data,
@Named(value = ThreadUtils.SCHEDULER_IO) Scheduler subscriberScheduler,
@Named(value = ThreadUtils.SCHEDULER_UI) Scheduler observerScheduler,
AccountMapping accountMapping) {
super(data, subscriberScheduler, observerScheduler);
this.accountMapping = accountMapping;
this.dao = new AccountDao(data);
}
@Override
public Single<Account> insertAccount(Account account) {
return Single.just(account)
.map(accountMapping::toModel)
.flatMap(dao::insert)
.map(model -> accountMapping.toEntity(model, account))
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Observable<Optional<Account>> observeAccount(UUID accountId) {
return dao.get(accountId).observableResult()
.map(result -> new Optional<>(result.firstOrNull()))
.map(accountMapping::toEntity)
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Maybe<Account> getAccount(UUID accountId) {
return dao.get(accountId).maybe()
.map(accountMapping::toEntity)
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Observable<Optional<Account>> observeAccountByAddress(String address) {
return dao.get(address).observableResult()
.map(result -> new Optional<>(result.firstOrNull()))
.map(accountMapping::toEntity)
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Maybe<Account> getAccountByAddress(String address) {
return dao.get(address).maybe()
.map(accountMapping::toEntity)
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Observable<List<Account>> observeAllAccounts() {
return dao.getAll().observableResult()
.map(ResultDelegate::toList)
.map(this::modelsToEntities)
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Observable<Account> observeAccounts() {
return dao.getAll().observableResult()
.flatMap(ReactiveResult::observable)
.map(accountMapping::toEntity)
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Single<Account> updateAccount(Account account) {
// Since we cannot access setId() of AccountModel, we have to query the model by ID and update it manually.
// https://github.com/requery/requery/issues/616#issuecomment-315685460
return dao.get(account.getId()).maybe().toSingle()
.map(model -> accountMapping.toModel(account, model))
.flatMap(updatedModel -> data().update(updatedModel))
.map(model -> accountMapping.toEntity(model, account))
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Single<Account> upsertAccount(Account account) {
return dao.get(account.getId()).maybe()
.switchIfEmpty(
Single.just(account).map(accountMapping::toModel).flatMap(dao::insert))
.map(model -> accountMapping.toModel(account, model))
.flatMap(data()::update)
.map(model -> accountMapping.toEntity(model, account))
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Completable deleteAccount(UUID accountId) {
return dao.delete(accountId).ignoreElement()
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
private List<Account> modelsToEntities(List<AccountModel> models) {
List<Account> entities = new ArrayList<>(models.size());
for (AccountModel model : models) {
entities.add(accountMapping.toEntity(model));
}
return entities;
}
}

View File

@ -0,0 +1,153 @@
package org.mercury_im.messenger.data.repository;
import org.mercury_im.messenger.data.mapping.DirectChatMapping;
import org.mercury_im.messenger.data.model.DirectChatModel;
import org.mercury_im.messenger.data.repository.dao.DirectChatDao;
import org.mercury_im.messenger.util.Optional;
import org.mercury_im.messenger.entity.chat.DirectChat;
import org.mercury_im.messenger.entity.chat.IDirectChat;
import org.mercury_im.messenger.entity.contact.Peer;
import org.mercury_im.messenger.util.ThreadUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.inject.Inject;
import javax.inject.Named;
import io.reactivex.Completable;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.Single;
import io.requery.Persistable;
import io.requery.query.ResultDelegate;
import io.requery.reactivex.ReactiveEntityStore;
import io.requery.reactivex.ReactiveResult;
public class XmppDirectChatRepository
extends RequeryRepository
implements DirectChatRepository {
private final DirectChatMapping directChatMapping;
private final DirectChatDao dao;
@Inject
public XmppDirectChatRepository(
ReactiveEntityStore<Persistable> data,
@Named(value = ThreadUtils.SCHEDULER_IO) Scheduler subscriberScheduler,
@Named(value = ThreadUtils.SCHEDULER_UI) Scheduler observerScheduler,
DirectChatMapping directChatMapping) {
super(data, subscriberScheduler, observerScheduler);
this.directChatMapping = directChatMapping;
this.dao = new DirectChatDao(data);
}
@Override
public Single<DirectChat> insertDirectChat(DirectChat chat) {
return Single.just(chat)
// map entity to model
.map(directChatMapping::toModel)
.flatMap(dao::insert)
// map back to entity
.map(model -> directChatMapping.toEntity(model, chat))
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Observable<Optional<DirectChat>> observeDirectChat(UUID chatId) {
return dao.get(chatId).observableResult()
.map(result -> new Optional<>(result.firstOrNull()))
.map(directChatMapping::toEntity)
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Maybe<DirectChat> getDirectChat(UUID chatId) {
return dao.get(chatId).maybe()
.map(directChatMapping::toEntity)
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Single<DirectChat> getOrCreateChatWithPeer(Peer peer) {
return getDirectChatByPeer(peer)
.switchIfEmpty(Single.just(new IDirectChat())
.map(chat -> {
chat.setAccount(peer.getAccount());
chat.setPeer(peer);
return chat;
})
.flatMap(this::insertDirectChat))
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Observable<Optional<DirectChat>> observeDirectChatByPeer(Peer peer) {
return dao.getByPeer(peer.getId()).observableResult()
.map(result -> new Optional<>(result.firstOrNull()))
.map(directChatMapping::toEntity)
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Maybe<DirectChat> getDirectChatByPeer(Peer peer) {
return dao.getByPeer(peer.getId()).maybe()
.map(directChatMapping::toEntity)
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Observable<List<DirectChat>> observeAllDirectChats() {
return dao.getAll().observableResult()
.map(ResultDelegate::toList)
.map(this::chatModelsToEntities)
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
private List<DirectChat> chatModelsToEntities(List<DirectChatModel> models) {
List<DirectChat> entities = new ArrayList<>(models.size());
for (DirectChatModel model : models) {
entities.add(directChatMapping.toEntity(model));
}
return entities;
}
@Override
public Single<DirectChat> updateDirectChat(DirectChat chat) {
return dao.get(chat.getId()).maybe().toSingle()
.map(model -> directChatMapping.toModel(chat, model))
.flatMap(data()::update)
.map(model -> directChatMapping.toEntity(model, chat))
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Single<DirectChat> upsertDirectChat(DirectChat chat) {
return dao.get(chat.getId()).maybe()
.switchIfEmpty(dao.insert(directChatMapping.toModel(chat)))
.map(directChatModel -> directChatMapping.toModel(chat, directChatModel))
.flatMap(data()::update)
.map(model -> directChatMapping.toEntity(model, chat))
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Completable deleteDirectChat(UUID chatId) {
return dao.delete(chatId)
.ignoreElement()
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
}

View File

@ -0,0 +1,91 @@
package org.mercury_im.messenger.data.repository;
import org.mercury_im.messenger.data.mapping.EntityCapsMapping;
import org.mercury_im.messenger.data.model.EntityCapsModel;
import org.mercury_im.messenger.entity.caps.EntityCapsRecord;
import org.mercury_im.messenger.util.ThreadUtils;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.inject.Inject;
import javax.inject.Named;
import io.reactivex.Completable;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.requery.Persistable;
import io.requery.query.Expression;
import io.requery.query.ResultDelegate;
import io.requery.reactivex.ReactiveEntityStore;
import io.requery.reactivex.ReactiveResult;
public class XmppEntityCapsRepository extends RequeryRepository implements EntityCapsRepository {
private final EntityCapsMapping entityCapsMapping;
@Inject
public XmppEntityCapsRepository(
ReactiveEntityStore<Persistable> data,
@Named(value = ThreadUtils.SCHEDULER_IO) Scheduler subscriberScheduler,
@Named(value = ThreadUtils.SCHEDULER_UI) Scheduler observerScheduler,
EntityCapsMapping mapping) {
super(data, subscriberScheduler, observerScheduler);
this.entityCapsMapping = mapping;
}
@Override
public Observable<Map<String, EntityCapsRecord>> observeAllEntityCapsRecords() {
return data().select(EntityCapsModel.class).get()
.observableResult()
.map(result -> result.toMap(EntityCapsModel.NODE_VER, new ConcurrentHashMap<>()))
.map(this::mapModelsToEntities)
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
private Map<String, EntityCapsRecord> mapModelsToEntities(Map<String, EntityCapsModel> models) {
Map<String, EntityCapsRecord> entities = new ConcurrentHashMap<>();
for (String key : models.keySet()) {
entities.put(key, entityCapsMapping.toEntity(models.get(key)));
}
return entities;
}
private Map<String, EntityCapsModel> mapEntitiesToModels(Map<String, EntityCapsRecord> entities) {
Map<String, EntityCapsModel> models = new ConcurrentHashMap<>();
for (String key : entities.keySet()) {
models.put(key, entityCapsMapping.toModel(entities.get(key)));
}
return models;
}
@Override
public Observable<EntityCapsRecord> observeEntityCapsRecords() {
return data().select(EntityCapsModel.class)
.get().observableResult()
.flatMap(ReactiveResult::observable)
.map(entityCapsMapping::toEntity)
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Maybe<EntityCapsRecord> maybeGetEntityCapsRecord(String nodeVer) {
return data().select(EntityCapsModel.class)
.where(EntityCapsModel.NODE_VER.eq(nodeVer))
.get().maybe()
.map(entityCapsMapping::toEntity)
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
@Override
public Completable insertEntityCapsRecord(EntityCapsRecord entityCapsRecord) {
return data().upsert(entityCapsMapping.toModel(entityCapsRecord))
.ignoreElement()
.subscribeOn(subscriberScheduler())
.observeOn(observerScheduler());
}
}

Some files were not shown because too many files have changed in this diff Show More